Commit 0d88379e by Calen Pennington

Make course ids and usage ids opaque to LMS and Studio [partial commit]

This commit adds all of cms.

These keys are now objects with a limited interface, and the particular
internal representation is managed by the data storage layer (the
modulestore).

For the LMS, there should be no outward-facing changes to the system.
The keys are, for now, a change to internal representation only. For
Studio, the new serialized form of the keys is used in urls, to allow
for further migration in the future.

Co-Author: Andy Armstrong <andya@edx.org>
Co-Author: Christina Roberts <christina@edx.org>
Co-Author: David Baumgold <db@edx.org>
Co-Author: Diana Huang <dkh@edx.org>
Co-Author: Don Mitchell <dmitchell@edx.org>
Co-Author: Julia Hansbrough <julia@edx.org>
Co-Author: Nimisha Asthagiri <nasthagiri@edx.org>
Co-Author: Sarina Canelake <sarina@edx.org>

[LMS-2370]
parent 7852906c
import ConfigParser import ConfigParser
from django.conf import settings from django.conf import settings
......
# pylint: disable=C0111 # pylint: disable=C0111
# pylint: disable=W0621 # pylint: disable=W0621
import time
import os import os
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true, assert_in # pylint: disable=no-name-in-module from nose.tools import assert_true, assert_in # pylint: disable=no-name-in-module
from django.conf import settings 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 student.models import get_user
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
...@@ -162,7 +161,7 @@ def add_course_author(user, course): ...@@ -162,7 +161,7 @@ def add_course_author(user, course):
""" """
global_admin = AdminFactory() global_admin = AdminFactory()
for role in (CourseStaffRole, CourseInstructorRole): 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(): def create_a_course():
...@@ -379,18 +378,17 @@ def create_other_user(_step, name, has_extra_perms, role_name): ...@@ -379,18 +378,17 @@ def create_other_user(_step, name, has_extra_perms, role_name):
user = create_studio_user(uname=name, password="test", email=email) user = create_studio_user(uname=name, password="test", email=email)
if has_extra_perms: if has_extra_perms:
if role_name == "is_staff": if role_name == "is_staff":
user.is_staff = True GlobalStaff().add_users(user)
user.save()
else: else:
if role_name == "admin": if role_name == "admin":
# admins get staff privileges, as well # admins get staff privileges, as well
roles = (CourseStaffRole, CourseInstructorRole) roles = (CourseStaffRole, CourseInstructorRole)
else: else:
roles = (CourseStaffRole,) roles = (CourseStaffRole,)
location = world.scenario_dict["COURSE"].location course_key = world.scenario_dict["COURSE"].id
global_admin = AdminFactory() global_admin = AdminFactory()
for role in roles: 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') @step('I log out')
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
from lettuce import world, step from lettuce import world, step
from component_settings_editor_helpers import enter_xml_in_advanced_problem from component_settings_editor_helpers import enter_xml_in_advanced_problem
from nose.tools import assert_true, assert_equal from nose.tools import assert_true, assert_equal
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from contentstore.utils import reverse_usage_url
@step('I export the course$') @step('I export the course$')
...@@ -43,4 +45,9 @@ def get_an_error_dialog(step): ...@@ -43,4 +45,9 @@ def get_an_error_dialog(step):
def i_click_on_error_dialog(step): def i_click_on_error_dialog(step):
world.click_link_by_text('Correct failed component') world.click_link_by_text('Correct failed component')
assert_true(world.css_html("span.inline-error").startswith("Problem i4x://MITx/999/problem")) 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 @@ ...@@ -4,8 +4,9 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from terrain.steps import reload_the_page from terrain.steps import reload_the_page
from selenium.common.exceptions import ( from selenium.common.exceptions import InvalidElementStateException
InvalidElementStateException, WebDriverException) 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 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): ...@@ -68,11 +69,12 @@ def change_assignment_name(step, old_name, new_name):
@step(u'I go back to the main course page') @step(u'I go back to the main course page')
def main_course_page(step): def main_course_page(step):
course_name = world.scenario_dict['COURSE'].display_name.replace(' ', '_') course_name = world.scenario_dict['COURSE'].display_name.replace(' ', '_')
main_page_link = '/course/{org}.{number}.{name}/branch/draft/block/{name}'.format( course_key = SlashSeparatedCourseKey(
org=world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].org,
number=world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].number,
name=course_name course_name
) )
main_page_link = reverse_course_url('course_handler', course_key)
world.visit(main_page_link) world.visit(main_page_link)
assert_in('Course Outline', world.css_text('h1.page-header')) assert_in('Course Outline', world.css_text('h1.page-header'))
......
...@@ -14,11 +14,11 @@ Feature: CMS.Sign in ...@@ -14,11 +14,11 @@ Feature: CMS.Sign in
Scenario: Login with a valid redirect Scenario: Login with a valid redirect
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I am not logged in 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 visit the url "/course/slashes:MITx+999+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 should see that the path is "/signin?next=/course/slashes%3AMITx%2B999%2BRobot_Super_Course"
When I fill in and submit the signin form When I fill in and submit the signin form
And I wait for "2" seconds 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 Scenario: Login with an invalid redirect
Given I have opened a new course in Studio Given I have opened a new course in Studio
...@@ -26,4 +26,4 @@ Feature: CMS.Sign in ...@@ -26,4 +26,4 @@ Feature: CMS.Sign in
And I visit the url "/signin?next=http://www.google.com/" And I visit the url "/signin?next=http://www.google.com/"
When I fill in and submit the signin form When I fill in and submit the signin form
And I wait for "2" seconds And I wait for "2" seconds
Then I should see that the path is "/course" Then I should see that the path is "/course/"
...@@ -165,8 +165,7 @@ def remove_transcripts_from_store(_step, subs_id): ...@@ -165,8 +165,7 @@ def remove_transcripts_from_store(_step, subs_id):
"""Remove from store, if transcripts content exists.""" """Remove from store, if transcripts content exists."""
filename = 'subs_{0}.srt.sjson'.format(subs_id.strip()) filename = 'subs_{0}.srt.sjson'.format(subs_id.strip())
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(
world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].id,
world.scenario_dict['COURSE'].number,
filename filename
) )
try: try:
......
...@@ -146,7 +146,7 @@ def user_foo_is_enrolled_in_the_course(step, name): ...@@ -146,7 +146,7 @@ def user_foo_is_enrolled_in_the_course(step, name):
world.create_user(name, 'test') world.create_user(name, 'test')
user = User.objects.get(username=name) 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) CourseEnrollment.enroll(user, course_id)
......
...@@ -140,10 +140,10 @@ def xml_only_video(step): ...@@ -140,10 +140,10 @@ def xml_only_video(step):
# Wait for the new unit to be created and to load the page # Wait for the new unit to be created and to load the page
world.wait(1) world.wait(1)
location = world.scenario_dict['COURSE'].location course = world.scenario_dict['COURSE']
store = get_modulestore(location) 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' youtube_id = 'ABCDEFG'
world.scenario_dict['YOUTUBE_ID'] = youtube_id world.scenario_dict['YOUTUBE_ID'] = youtube_id
......
...@@ -14,7 +14,6 @@ from django.utils import timezone ...@@ -14,7 +14,6 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
...@@ -64,13 +63,10 @@ def cmd_log(cmd, cwd): ...@@ -64,13 +63,10 @@ def cmd_log(cmd, cwd):
return output 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.""" """Export a course to git."""
# pylint: disable=R0915 # pylint: disable=R0915
if course_loc.startswith('i4x://'):
course_loc = course_loc[6:]
if not GIT_REPO_EXPORT_DIR: if not GIT_REPO_EXPORT_DIR:
raise GitExportError(GitExportError.NO_EXPORT_DIR) raise GitExportError(GitExportError.NO_EXPORT_DIR)
...@@ -129,15 +125,10 @@ def export_to_git(course_loc, repo, user='', rdir=None): ...@@ -129,15 +125,10 @@ def export_to_git(course_loc, repo, user='', rdir=None):
raise GitExportError(GitExportError.CANNOT_PULL) raise GitExportError(GitExportError.CANNOT_PULL)
# export course as xml before commiting and pushing # 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) root_dir = os.path.dirname(rdirp)
course_dir = os.path.splitext(os.path.basename(rdirp))[0] course_dir = os.path.splitext(os.path.basename(rdirp))[0]
try: try:
export_to_xml(modulestore('direct'), contentstore(), location, export_to_xml(modulestore('direct'), contentstore(), course_id,
root_dir, course_dir, modulestore()) root_dir, course_dir, modulestore())
except (EnvironmentError, AttributeError): except (EnvironmentError, AttributeError):
log.exception('Failed export to xml') log.exception('Failed export to xml')
......
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor from xmodule.modulestore.keys import CourseKey
from xmodule.modulestore import Location
class Command(BaseCommand): class Command(BaseCommand):
...@@ -10,14 +9,13 @@ class Command(BaseCommand): ...@@ -10,14 +9,13 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 1: 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] course_key = CourseKey.from_string(args[0])
loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore() store = modulestore()
course = store.get_item(loc, depth=3) course = store.get_course(course_key, depth=3)
err_cnt = 0 err_cnt = 0
...@@ -33,7 +31,7 @@ class Command(BaseCommand): ...@@ -33,7 +31,7 @@ class Command(BaseCommand):
def _check_xml_attributes_field(module): def _check_xml_attributes_field(module):
err_cnt = 0 err_cnt = 0
if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring): 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 err_cnt = err_cnt + 1
for child in module.get_children(): for child in module.get_children():
err_cnt = err_cnt + _check_xml_attributes_field(child) err_cnt = err_cnt + _check_xml_attributes_field(child)
...@@ -45,7 +43,7 @@ class Command(BaseCommand): ...@@ -45,7 +43,7 @@ class Command(BaseCommand):
def _get_discussion_items(module): def _get_discussion_items(module):
discussion_items = [] discussion_items = []
if module.location.category == 'discussion': if module.location.category == 'discussion':
discussion_items = discussion_items + [module.location.url()] discussion_items = discussion_items + [module.location]
for child in module.get_children(): for child in module.get_children():
discussion_items = discussion_items + _get_discussion_items(child) discussion_items = discussion_items + _get_discussion_items(child)
...@@ -55,17 +53,8 @@ class Command(BaseCommand): ...@@ -55,17 +53,8 @@ class Command(BaseCommand):
discussion_items = _get_discussion_items(course) discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal # now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items( queried_discussion_items = store.get_items(course_key=course_key, category='discussion',)
Location(
'i4x',
course.location.org,
course.location.course,
'discussion',
None,
None
)
)
for item in queried_discussion_items: for item in queried_discussion_items:
if item.location.url() not in discussion_items: if item.location not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url()) print 'Found dangling discussion module = {0}'.format(item.location)
...@@ -5,9 +5,8 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -5,9 +5,8 @@ from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
from xmodule.modulestore import Location from xmodule.modulestore.keys import CourseKey
# #
...@@ -22,29 +21,22 @@ class Command(BaseCommand): ...@@ -22,29 +21,22 @@ class Command(BaseCommand):
if len(args) != 2: if len(args) != 2:
raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>") raise CommandError("clone requires 2 arguments: <source-course_id> <dest-course_id>")
source_course_id = args[0] source_course_id = CourseKey.from_string(args[0])
dest_course_id = args[1] dest_course_id = CourseKey.from_string(args[1])
mstore = modulestore('direct') mstore = modulestore('direct')
cstore = contentstore() cstore = contentstore()
course_id_dict = Location.parse_course_id(dest_course_id) mstore.ignore_write_events_on_courses.add(dest_course_id)
mstore.ignore_write_events_on_courses.append('{org}/{course}'.format(**course_id_dict))
print("Cloning course {0} to {1}".format(source_course_id, 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) if clone_course(mstore, cstore, source_course_id, dest_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)
print("copying User permissions...") print("copying User permissions...")
# purposely avoids auth.add_user b/c it doesn't have a caller to authorize # purposely avoids auth.add_user b/c it doesn't have a caller to authorize
CourseInstructorRole(dest_location).add_users( CourseInstructorRole(dest_course_id).add_users(
*CourseInstructorRole(source_location).users_with_role() *CourseInstructorRole(source_course_id).users_with_role()
) )
CourseStaffRole(dest_location).add_users( CourseStaffRole(dest_course_id).add_users(
*CourseStaffRole(source_location).users_with_role() *CourseStaffRole(source_course_id).users_with_role()
) )
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from .prompt import query_yes_no from .prompt import query_yes_no
from contentstore.utils import delete_course_and_groups from contentstore.utils import delete_course_and_groups
from xmodule.modulestore.keys import CourseKey
class Command(BaseCommand): class Command(BaseCommand):
...@@ -11,9 +12,9 @@ class Command(BaseCommand): ...@@ -11,9 +12,9 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 1 and len(args) != 2: 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] course_id = CourseKey.from_string(args[0])
commit = False commit = False
if len(args) == 2: if len(args) == 2:
......
...@@ -13,6 +13,9 @@ from .prompt import query_yes_no ...@@ -13,6 +13,9 @@ from .prompt import query_yes_no
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from contentstore.views import tabs 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): def print_course(course):
...@@ -64,7 +67,11 @@ command again, adding --insert or --delete to edit the list. ...@@ -64,7 +67,11 @@ command again, adding --insert or --delete to edit the list.
if not options['course']: if not options['course']:
raise CommandError(Command.course_option.help) 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 'Warning: this command directly edits the list of course tabs in mongo.'
print 'Tabs before any changes:' print 'Tabs before any changes:'
......
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.course_module import CourseDescriptor
from xmodule.contentstore.utils import empty_asset_trashcan from xmodule.contentstore.utils import empty_asset_trashcan
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.keys import CourseKey
from .prompt import query_yes_no from .prompt import query_yes_no
...@@ -10,16 +10,12 @@ class Command(BaseCommand): ...@@ -10,16 +10,12 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 1 and len(args) != 0: if len(args) != 1 and len(args) != 0:
raise CommandError("empty_asset_trashcan requires one or no arguments: |<location>|") raise CommandError("empty_asset_trashcan requires one or no arguments: |<course_id>|")
locs = []
if len(args) == 1: if len(args) == 1:
locs.append(CourseDescriptor.id_to_location(args[0])) course_ids = [CourseKey.from_string(args[0])]
else: else:
courses = modulestore('direct').get_courses() course_ids = [course.id for course in modulestore('direct').get_courses()]
for course in courses:
locs.append(course.location)
if query_yes_no("Emptying trashcan. Confirm?", default="no"): if query_yes_no("Emptying trashcan. Confirm?", default="no"):
empty_asset_trashcan(locs) empty_asset_trashcan(course_ids)
...@@ -6,6 +6,7 @@ import os ...@@ -6,6 +6,7 @@ import os
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.keys import CourseKey
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -19,16 +20,14 @@ class Command(BaseCommand): ...@@ -19,16 +20,14 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
"Execute the command" "Execute the command"
if len(args) != 2: if len(args) != 2:
raise CommandError("export requires two arguments: <course location> <output path>") raise CommandError("export requires two arguments: <course id> <output path>")
course_id = args[0] course_id = CourseKey.from_string(args[0])
output_path = args[1] output_path = args[1]
print("Exporting course id = {0} to {1}".format(course_id, output_path)) print("Exporting course id = {0} to {1}".format(course_id, output_path))
location = CourseDescriptor.id_to_location(course_id)
root_dir = os.path.dirname(output_path) root_dir = os.path.dirname(output_path)
course_dir = os.path.splitext(os.path.basename(output_path))[0] 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_id, root_dir, course_dir, modulestore())
...@@ -35,9 +35,8 @@ class Command(BaseCommand): ...@@ -35,9 +35,8 @@ class Command(BaseCommand):
if 1: if 1:
try: try:
location = CourseDescriptor.id_to_location(course_id)
course_dir = course_id.replace('/', '...') 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: except Exception as err:
print("="*30 + "> Oops, failed to export %s" % course_id) print("="*30 + "> Oops, failed to export %s" % course_id)
print("Error:") print("Error:")
......
...@@ -20,6 +20,10 @@ from django.core.management.base import BaseCommand, CommandError ...@@ -20,6 +20,10 @@ from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import contentstore.git_export_utils as git_export_utils 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__) log = logging.getLogger(__name__)
...@@ -53,8 +57,16 @@ class Command(BaseCommand): ...@@ -53,8 +57,16 @@ class Command(BaseCommand):
# Rethrow GitExportError as CommandError for SystemExit # Rethrow GitExportError as CommandError for SystemExit
try: 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( git_export_utils.export_to_git(
args[0], course_key,
args[1], args[1],
options.get('user', ''), options.get('user', ''),
options.get('rdir', None) options.get('rdir', None)
......
...@@ -47,11 +47,12 @@ class Command(BaseCommand): ...@@ -47,11 +47,12 @@ class Command(BaseCommand):
_, course_items = import_from_xml( _, course_items = import_from_xml(
mstore, data_dir, course_dirs, load_error_modules=False, mstore, data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True, 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: for course in course_items:
course_id = module.location.course_id course_id = course.id
if not are_permissions_roles_seeded(course_id): if not are_permissions_roles_seeded(course_id):
self.stdout.write('Seeding forum roles for course {0}\n'.format(course_id)) self.stdout.write('Seeding forum roles for course {0}\n'.format(course_id))
seed_permissions_roles(course_id) seed_permissions_roles(course_id)
"""
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,10 +4,8 @@ to the new split-Mongo modulestore. ...@@ -4,10 +4,8 @@ to the new split-Mongo modulestore.
""" """
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.split_migrator import SplitMigrator from xmodule.modulestore.split_migrator import SplitMigrator
from xmodule.modulestore import InvalidLocationError
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
...@@ -30,13 +28,12 @@ class Command(BaseCommand): ...@@ -30,13 +28,12 @@ class Command(BaseCommand):
"Migrate a course from old-Mongo to split-Mongo" "Migrate a course from old-Mongo to split-Mongo"
help = "Migrate a course from old-Mongo to split-Mongo" help = "Migrate a course from old-Mongo to split-Mongo"
args = "location email <locator>" args = "location email <new org> <new offering>"
def parse_args(self, *args): def parse_args(self, *args):
""" """
Return a three-tuple of (location, user, locator_string). Return a 4-tuple of (location, user, org, offering).
If the user didn't specify a locator string, the third return value If the user didn't specify an org & offering, those will be None.
will be None.
""" """
if len(args) < 2: if len(args) < 2:
raise CommandError( raise CommandError(
...@@ -44,10 +41,7 @@ class Command(BaseCommand): ...@@ -44,10 +41,7 @@ class Command(BaseCommand):
"a location and a user identifier (email or ID)" "a location and a user identifier (email or ID)"
) )
try: location = args[0]
location = Location(args[0])
except InvalidLocationError:
raise CommandError("Invalid location string {}".format(args[0]))
try: try:
user = user_from_str(args[1]) user = user_from_str(args[1])
...@@ -55,14 +49,15 @@ class Command(BaseCommand): ...@@ -55,14 +49,15 @@ class Command(BaseCommand):
raise CommandError("No user found identified by {}".format(args[1])) raise CommandError("No user found identified by {}".format(args[1]))
try: try:
package_id = args[2] org = args[2]
offering = args[3]
except IndexError: except IndexError:
package_id = None org = offering = None
return location, user, package_id return location, user, org, offering
def handle(self, *args, **options): def handle(self, *args, **options):
location, user, package_id = self.parse_args(*args) location, user, org, offering = self.parse_args(*args)
migrator = SplitMigrator( migrator = SplitMigrator(
draft_modulestore=modulestore('default'), draft_modulestore=modulestore('default'),
...@@ -71,4 +66,4 @@ class Command(BaseCommand): ...@@ -71,4 +66,4 @@ class Command(BaseCommand):
loc_mapper=loc_mapper(), loc_mapper=loc_mapper(),
) )
migrator.migrate_mongo_course(location, user, package_id) migrator.migrate_mongo_course(location, user, org, offering)
...@@ -4,7 +4,7 @@ is to delete the course from the split mongo datastore. ...@@ -4,7 +4,7 @@ is to delete the course from the split mongo datastore.
""" """
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore, loc_mapper 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 from xmodule.modulestore.locator import CourseLocator
...@@ -12,18 +12,18 @@ class Command(BaseCommand): ...@@ -12,18 +12,18 @@ class Command(BaseCommand):
"Rollback a course that was migrated to the split Mongo datastore" "Rollback a course that was migrated to the split Mongo datastore"
help = "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): def handle(self, *args, **options):
if len(args) < 1: if len(args) < 2:
raise CommandError( raise CommandError(
"rollback_split_course requires at least one argument (locator)" "rollback_split_course requires 2 arguments (org offering)"
) )
try: try:
locator = CourseLocator(url=args[0]) locator = CourseLocator(org=args[0], offering=args[1])
except ValueError: 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) location = loc_mapper().translate_locator_to_location(locator, get_course=True)
if not location: if not location:
...@@ -41,7 +41,7 @@ class Command(BaseCommand): ...@@ -41,7 +41,7 @@ class Command(BaseCommand):
) )
try: try:
modulestore('split').delete_course(locator.package_id) modulestore('split').delete_course(locator)
except ItemNotFoundError: except ItemNotFoundError:
raise CommandError("No course found with locator {}".format(locator)) raise CommandError("No course found with locator {}".format(locator))
......
...@@ -15,19 +15,19 @@ class ClashIdTestCase(TestCase): ...@@ -15,19 +15,19 @@ class ClashIdTestCase(TestCase):
expected = [] expected = []
# clashing courses # clashing courses
course = CourseFactory.create(org="test", course="courseid", display_name="run1") 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") 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") course = CourseFactory.create(org="test", course="CourseId", display_name="aRUN123")
expected.append(course.location.course_id) expected.append(course.id)
# not clashing courses # not clashing courses
not_expected = [] not_expected = []
course = CourseFactory.create(org="test", course="course2", display_name="run1") 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") 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") 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 old_stdout = sys.stdout
sys.stdout = mystdout = StringIO() sys.stdout = mystdout = StringIO()
...@@ -35,6 +35,6 @@ class ClashIdTestCase(TestCase): ...@@ -35,6 +35,6 @@ class ClashIdTestCase(TestCase):
sys.stdout = old_stdout sys.stdout = old_stdout
result = mystdout.getvalue() result = mystdout.getvalue()
for courseid in expected: for courseid in expected:
self.assertIn(courseid, result) self.assertIn(courseid.to_deprecated_string(), result)
for courseid in not_expected: 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 ...@@ -18,6 +18,7 @@ from django.test.utils import override_settings
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils import contentstore.git_export_utils as git_export_utils
from contentstore.git_export_utils import GitExportError from contentstore.git_export_utils import GitExportError
from xmodule.modulestore.locations import SlashSeparatedCourseKey
FEATURES_WITH_EXPORT_GIT = settings.FEATURES.copy() FEATURES_WITH_EXPORT_GIT = settings.FEATURES.copy()
FEATURES_WITH_EXPORT_GIT['ENABLE_EXPORT_GIT'] = True FEATURES_WITH_EXPORT_GIT['ENABLE_EXPORT_GIT'] = True
...@@ -52,7 +53,7 @@ class TestGitExport(CourseTestCase): ...@@ -52,7 +53,7 @@ class TestGitExport(CourseTestCase):
def test_command(self): 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. test output.
""" """
with self.assertRaises(SystemExit) as ex: with self.assertRaises(SystemExit) as ex:
...@@ -69,7 +70,13 @@ class TestGitExport(CourseTestCase): ...@@ -69,7 +70,13 @@ class TestGitExport(CourseTestCase):
# Send bad url to get course not exported # Send bad url to get course not exported
with self.assertRaises(SystemExit) as ex: with self.assertRaises(SystemExit) as ex:
with self.assertRaisesRegexp(CommandError, GitExportError.URL_BAD): 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()) stderr=StringIO.StringIO())
self.assertEqual(ex.exception.code, 1) self.assertEqual(ex.exception.code, 1)
...@@ -77,15 +84,16 @@ class TestGitExport(CourseTestCase): ...@@ -77,15 +84,16 @@ class TestGitExport(CourseTestCase):
""" """
Test several bad URLs for validation Test several bad URLs for validation
""" """
course_key = SlashSeparatedCourseKey('org', 'course', 'run')
with self.assertRaisesRegexp(GitExportError, str(GitExportError.URL_BAD)): 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)): 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, with self.assertRaisesRegexp(GitExportError,
str(GitExportError.URL_NO_AUTH)): 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): def test_bad_git_repos(self):
""" """
...@@ -93,11 +101,12 @@ class TestGitExport(CourseTestCase): ...@@ -93,11 +101,12 @@ class TestGitExport(CourseTestCase):
""" """
test_repo_path = '{}/test_repo'.format(git_export_utils.GIT_REPO_EXPORT_DIR) test_repo_path = '{}/test_repo'.format(git_export_utils.GIT_REPO_EXPORT_DIR)
self.assertFalse(os.path.isdir(test_repo_path)) self.assertFalse(os.path.isdir(test_repo_path))
course_key = SlashSeparatedCourseKey('foo', 'blah', '100-')
# Test bad clones # Test bad clones
with self.assertRaisesRegexp(GitExportError, with self.assertRaisesRegexp(GitExportError,
str(GitExportError.CANNOT_PULL)): str(GitExportError.CANNOT_PULL)):
git_export_utils.export_to_git( git_export_utils.export_to_git(
'foo/blah/100', course_key,
'https://user:blah@example.com/test_repo.git') 'https://user:blah@example.com/test_repo.git')
self.assertFalse(os.path.isdir(test_repo_path)) self.assertFalse(os.path.isdir(test_repo_path))
...@@ -105,24 +114,16 @@ class TestGitExport(CourseTestCase): ...@@ -105,24 +114,16 @@ class TestGitExport(CourseTestCase):
with self.assertRaisesRegexp(GitExportError, with self.assertRaisesRegexp(GitExportError,
str(GitExportError.XML_EXPORT_FAIL)): str(GitExportError.XML_EXPORT_FAIL)):
git_export_utils.export_to_git( git_export_utils.export_to_git(
'foo/blah/100', course_key,
'file://{0}'.format(self.bare_repo_dir)) 'file://{0}'.format(self.bare_repo_dir))
# Test bad git remote after successful clone # Test bad git remote after successful clone
with self.assertRaisesRegexp(GitExportError, with self.assertRaisesRegexp(GitExportError,
str(GitExportError.CANNOT_PULL)): str(GitExportError.CANNOT_PULL)):
git_export_utils.export_to_git( git_export_utils.export_to_git(
'foo/blah/100', course_key,
'https://user:blah@example.com/r.git') '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 @unittest.skipIf(os.environ.get('GIT_CONFIG') or
os.environ.get('GIT_AUTHOR_EMAIL') or os.environ.get('GIT_AUTHOR_EMAIL') or
os.environ.get('GIT_AUTHOR_NAME') or os.environ.get('GIT_AUTHOR_NAME') or
...@@ -170,7 +171,7 @@ class TestGitExport(CourseTestCase): ...@@ -170,7 +171,7 @@ class TestGitExport(CourseTestCase):
Test response if there are no changes Test response if there are no changes
""" """
git_export_utils.export_to_git( git_export_utils.export_to_git(
'i4x://{0}'.format(self.course.id), self.course.id,
'file://{0}'.format(self.bare_repo_dir) 'file://{0}'.format(self.bare_repo_dir)
) )
......
...@@ -14,6 +14,7 @@ from contentstore.tests.modulestore_config import TEST_MODULESTORE ...@@ -14,6 +14,7 @@ from contentstore.tests.modulestore_config import TEST_MODULESTORE
from django_comment_common.utils import are_permissions_roles_seeded from django_comment_common.utils import are_permissions_roles_seeded
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
...@@ -22,8 +23,8 @@ class TestImport(ModuleStoreTestCase): ...@@ -22,8 +23,8 @@ class TestImport(ModuleStoreTestCase):
Unit tests for importing a course from command line Unit tests for importing a course from command line
""" """
COURSE_ID = ['EDx', '0.00x', '2013_Spring', ] COURSE_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2013_Spring')
DIFF_RUN = ['EDx', '0.00x', '2014_Spring', ] DIFF_KEY = SlashSeparatedCourseKey(u'edX', u'test_import_course', u'2014_Spring')
def setUp(self): def setUp(self):
""" """
...@@ -37,29 +38,29 @@ class TestImport(ModuleStoreTestCase): ...@@ -37,29 +38,29 @@ class TestImport(ModuleStoreTestCase):
self.good_dir = tempfile.mkdtemp(dir=self.content_dir) self.good_dir = tempfile.mkdtemp(dir=self.content_dir)
os.makedirs(os.path.join(self.good_dir, "course")) os.makedirs(os.path.join(self.good_dir, "course"))
with open(os.path.join(self.good_dir, "course.xml"), "w+") as f: with open(os.path.join(self.good_dir, "course.xml"), "w+") as f:
f.write('<course url_name="{0[2]}" org="{0[0]}" ' f.write('<course url_name="{0.run}" org="{0.org}" '
'course="{0[1]}"/>'.format(self.COURSE_ID)) 'course="{0.course}"/>'.format(self.COURSE_KEY))
with open(os.path.join(self.good_dir, "course", "{0[2]}.xml".format(self.COURSE_ID)), "w+") as f: with open(os.path.join(self.good_dir, "course", "{0.run}.xml".format(self.COURSE_KEY)), "w+") as f:
f.write('<course></course>') f.write('<course></course>')
# Create run changed course xml # Create run changed course xml
self.dupe_dir = tempfile.mkdtemp(dir=self.content_dir) self.dupe_dir = tempfile.mkdtemp(dir=self.content_dir)
os.makedirs(os.path.join(self.dupe_dir, "course")) os.makedirs(os.path.join(self.dupe_dir, "course"))
with open(os.path.join(self.dupe_dir, "course.xml"), "w+") as f: with open(os.path.join(self.dupe_dir, "course.xml"), "w+") as f:
f.write('<course url_name="{0[2]}" org="{0[0]}" ' f.write('<course url_name="{0.run}" org="{0.org}" '
'course="{0[1]}"/>'.format(self.DIFF_RUN)) 'course="{0.course}"/>'.format(self.DIFF_KEY))
with open(os.path.join(self.dupe_dir, "course", "{0[2]}.xml".format(self.DIFF_RUN)), "w+") as f: with open(os.path.join(self.dupe_dir, "course", "{0.run}.xml".format(self.DIFF_KEY)), "w+") as f:
f.write('<course></course>') f.write('<course></course>')
def test_forum_seed(self): def test_forum_seed(self):
""" """
Tests that forum roles were created with import. Tests that forum roles were created with import.
""" """
self.assertFalse(are_permissions_roles_seeded('/'.join(self.COURSE_ID))) self.assertFalse(are_permissions_roles_seeded(self.COURSE_KEY))
call_command('import', self.content_dir, self.good_dir) call_command('import', self.content_dir, self.good_dir)
self.assertTrue(are_permissions_roles_seeded('/'.join(self.COURSE_ID))) self.assertTrue(are_permissions_roles_seeded(self.COURSE_KEY))
def test_duplicate_with_url(self): def test_duplicate_with_url(self):
""" """
...@@ -70,8 +71,9 @@ class TestImport(ModuleStoreTestCase): ...@@ -70,8 +71,9 @@ class TestImport(ModuleStoreTestCase):
# Load up base course and verify it is available # Load up base course and verify it is available
call_command('import', self.content_dir, self.good_dir) call_command('import', self.content_dir, self.good_dir)
store = modulestore() store = modulestore()
self.assertIsNotNone(store.get_course('/'.join(self.COURSE_ID))) self.assertIsNotNone(store.get_course(self.COURSE_KEY))
# Now load up duped course and verify it doesn't load # Now load up duped course and verify it doesn't load
call_command('import', self.content_dir, self.dupe_dir) 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))
self.assertTrue(are_permissions_roles_seeded(self.COURSE_KEY))
...@@ -10,11 +10,12 @@ from contentstore.management.commands.migrate_to_split import Command ...@@ -10,11 +10,12 @@ from contentstore.management.commands.migrate_to_split import Command
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory 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 from xmodule.modulestore.locator import CourseLocator
# pylint: disable=E1101 # pylint: disable=E1101
@unittest.skip("Not fixing split mongo until we land this long branch")
class TestArgParsing(unittest.TestCase): class TestArgParsing(unittest.TestCase):
""" """
Tests for parsing arguments for the `migrate_to_split` management command Tests for parsing arguments for the `migrate_to_split` management command
...@@ -43,6 +44,7 @@ class TestArgParsing(unittest.TestCase): ...@@ -43,6 +44,7 @@ class TestArgParsing(unittest.TestCase):
self.command.handle("i4x://org/course/category/name", "fake@example.com") 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) @override_settings(MODULESTORE=TEST_MODULESTORE)
class TestMigrateToSplit(ModuleStoreTestCase): class TestMigrateToSplit(ModuleStoreTestCase):
""" """
...@@ -65,8 +67,7 @@ class TestMigrateToSplit(ModuleStoreTestCase): ...@@ -65,8 +67,7 @@ class TestMigrateToSplit(ModuleStoreTestCase):
str(self.course.location), str(self.course.location),
str(self.user.email), str(self.user.email),
) )
locator = loc_mapper().translate_location(self.course.id, self.course.location) course_from_split = modulestore('split').get_course(self.course.id)
course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split) self.assertIsNotNone(course_from_split)
def test_user_id(self): def test_user_id(self):
...@@ -75,8 +76,7 @@ class TestMigrateToSplit(ModuleStoreTestCase): ...@@ -75,8 +76,7 @@ class TestMigrateToSplit(ModuleStoreTestCase):
str(self.course.location), str(self.course.location),
str(self.user.id), str(self.user.id),
) )
locator = loc_mapper().translate_location(self.course.id, self.course.location) course_from_split = modulestore('split').get_course(self.course.id)
course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split) self.assertIsNotNone(course_from_split)
def test_locator_string(self): def test_locator_string(self):
...@@ -84,8 +84,8 @@ class TestMigrateToSplit(ModuleStoreTestCase): ...@@ -84,8 +84,8 @@ class TestMigrateToSplit(ModuleStoreTestCase):
"migrate_to_split", "migrate_to_split",
str(self.course.location), str(self.course.location),
str(self.user.id), 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) course_from_split = modulestore('split').get_course(locator)
self.assertIsNotNone(course_from_split) self.assertIsNotNone(course_from_split)
...@@ -19,6 +19,7 @@ from xmodule.modulestore.split_migrator import SplitMigrator ...@@ -19,6 +19,7 @@ from xmodule.modulestore.split_migrator import SplitMigrator
# pylint: disable=E1101 # pylint: disable=E1101
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
class TestArgParsing(unittest.TestCase): class TestArgParsing(unittest.TestCase):
""" """
Tests for parsing arguments for the `rollback_split_course` management command Tests for parsing arguments for the `rollback_split_course` management command
...@@ -37,6 +38,7 @@ class TestArgParsing(unittest.TestCase): ...@@ -37,6 +38,7 @@ class TestArgParsing(unittest.TestCase):
self.command.handle("!?!") self.command.handle("!?!")
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase): class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase):
""" """
...@@ -54,6 +56,8 @@ class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase): ...@@ -54,6 +56,8 @@ class TestRollbackSplitCourseNoOldMongo(ModuleStoreTestCase):
with self.assertRaisesRegexp(CommandError, errstring): with self.assertRaisesRegexp(CommandError, errstring):
Command().handle(str(locator)) Command().handle(str(locator))
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase): class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase):
""" """
...@@ -66,12 +70,13 @@ class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase): ...@@ -66,12 +70,13 @@ class TestRollbackSplitCourseNoSplitMongo(ModuleStoreTestCase):
self.old_course = CourseFactory() self.old_course = CourseFactory()
def test_nonexistent_locator(self): 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" errstring = "No course found with locator"
with self.assertRaisesRegexp(CommandError, errstring): with self.assertRaisesRegexp(CommandError, errstring):
Command().handle(str(locator)) Command().handle(str(locator))
@unittest.skip("Not fixing split mongo until we land opaque-keys 0.9")
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class TestRollbackSplitCourse(ModuleStoreTestCase): class TestRollbackSplitCourse(ModuleStoreTestCase):
""" """
...@@ -93,18 +98,17 @@ class TestRollbackSplitCourse(ModuleStoreTestCase): ...@@ -93,18 +98,17 @@ class TestRollbackSplitCourse(ModuleStoreTestCase):
loc_mapper=loc_mapper(), loc_mapper=loc_mapper(),
) )
migrator.migrate_mongo_course(self.old_course.location, self.user) 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(self.old_course.id)
self.course = modulestore('split').get_course(locator)
@patch("sys.stdout", new_callable=StringIO) @patch("sys.stdout", new_callable=StringIO)
def test_happy_path(self, mock_stdout): def test_happy_path(self, mock_stdout):
locator = self.course.location course_id = self.course.id
call_command( call_command(
"rollback_split_course", "rollback_split_course",
str(locator), str(course_id),
) )
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
modulestore('split').get_course(locator) modulestore('split').get_course(course_id)
self.assertIn("Course rolled back successfully", mock_stdout.getvalue()) self.assertIn("Course rolled back successfully", mock_stdout.getvalue())
...@@ -15,9 +15,9 @@ class Content: ...@@ -15,9 +15,9 @@ class Content:
class CachingTestCase(TestCase): class CachingTestCase(TestCase):
# Tests for https://edx.lighthouseapp.com/projects/102637/tickets/112-updating-asset-does-not-refresh-the-cached-copy # 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 # 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') mockAsset = Content(unicodeLocation, 'my content')
def test_put_and_get(self): def test_put_and_get(self):
......
...@@ -6,12 +6,13 @@ from xmodule.course_module import CourseDescriptor ...@@ -6,12 +6,13 @@ from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore, loc_mapper, clear_existing_modulestores from xmodule.modulestore.django import modulestore, loc_mapper, clear_existing_modulestores
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.capa_module import CapaDescriptor 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.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from xmodule.html_module import HtmlDescriptor from xmodule.html_module import HtmlDescriptor
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@unittest.skip("Not fixing split until we land opaque-keys 0.9")
class TemplateTests(unittest.TestCase): class TemplateTests(unittest.TestCase):
""" """
Test finding and using the templates (boilerplates) for xblocks. Test finding and using the templates (boilerplates) for xblocks.
...@@ -67,7 +68,7 @@ class TemplateTests(unittest.TestCase): ...@@ -67,7 +68,7 @@ class TemplateTests(unittest.TestCase):
parent_location=test_course.location) parent_location=test_course.location)
self.assertIsInstance(test_chapter, SequenceDescriptor) self.assertIsInstance(test_chapter, SequenceDescriptor)
# refetch parent which should now point to child # 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)
self.assertIn(test_chapter.location.block_id, test_course.children) self.assertIn(test_chapter.location.block_id, test_course.children)
with self.assertRaises(DuplicateCourseError): with self.assertRaises(DuplicateCourseError):
...@@ -153,17 +154,17 @@ class TemplateTests(unittest.TestCase): ...@@ -153,17 +154,17 @@ class TemplateTests(unittest.TestCase):
persistent_factories.ItemFactory.create(display_name='chapter 1', persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location) parent_location=test_course.location)
id_locator = CourseLocator(package_id=test_course.location.package_id, branch='draft') id_locator = test_course.id.for_branch('draft')
guid_locator = CourseLocator(version_guid=test_course.location.version_guid) guid_locator = test_course.location.course_agnostic()
# verify it can be retireved by id # verify it can be retrieved by id
self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor) self.assertIsInstance(modulestore('split').get_course(id_locator), CourseDescriptor)
# and by guid # and by guid
self.assertIsInstance(modulestore('split').get_course(guid_locator), CourseDescriptor) self.assertIsInstance(modulestore('split').get_item(guid_locator), CourseDescriptor)
modulestore('split').delete_course(id_locator.package_id) modulestore('split').delete_course(id_locator)
# test can no longer retrieve by id # test can no longer retrieve by id
self.assertRaises(ItemNotFoundError, modulestore('split').get_course, id_locator) self.assertRaises(ItemNotFoundError, modulestore('split').get_course, id_locator)
# but can by guid # 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): def test_block_generations(self):
""" """
...@@ -192,7 +193,7 @@ class TemplateTests(unittest.TestCase): ...@@ -192,7 +193,7 @@ class TemplateTests(unittest.TestCase):
second_problem = persistent_factories.ItemFactory.create( second_problem = persistent_factories.ItemFactory.create(
display_name='problem 2', display_name='problem 2',
parent_location=BlockUsageLocator(updated_loc, block_id=sub.location.block_id), parent_location=BlockUsageLocator.make_relative(updated_loc, block_id=sub.location.block_id),
user_id='testbot', category='problem', user_id='testbot', category='problem',
data="<problem></problem>" data="<problem></problem>"
) )
......
...@@ -9,7 +9,6 @@ import subprocess ...@@ -9,7 +9,6 @@ import subprocess
from uuid import uuid4 from uuid import uuid4
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from pymongo import MongoClient from pymongo import MongoClient
...@@ -17,7 +16,7 @@ from .utils import CourseTestCase ...@@ -17,7 +16,7 @@ from .utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils import contentstore.git_export_utils as git_export_utils
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import modulestore 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 = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
...@@ -34,12 +33,8 @@ class TestExportGit(CourseTestCase): ...@@ -34,12 +33,8 @@ class TestExportGit(CourseTestCase):
Setup test course, user, and url. Setup test course, user, and url.
""" """
super(TestExportGit, self).setUp() super(TestExportGit, self).setUp()
self.course_module = modulestore().get_item(self.course.location) self.course_module = modulestore().get_course(self.course.id)
self.test_url = reverse('export_git', kwargs={ self.test_url = reverse_course_url('export_git', self.course.id)
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
def tearDown(self): def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db']) MongoClient().drop_database(TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'])
......
...@@ -47,7 +47,7 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -47,7 +47,7 @@ class InternationalizationTest(ModuleStoreTestCase):
self.client = AjaxEnabledTestClient() self.client = AjaxEnabledTestClient()
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get_html('/course') resp = self.client.get_html('/course/')
self.assertContains(resp, self.assertContains(resp,
'<h1 class="page-header">My Courses</h1>', '<h1 class="page-header">My Courses</h1>',
status_code=200, status_code=200,
...@@ -58,7 +58,7 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -58,7 +58,7 @@ class InternationalizationTest(ModuleStoreTestCase):
self.client = AjaxEnabledTestClient() self.client = AjaxEnabledTestClient()
self.client.login(username=self.uname, password=self.password) 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' HTTP_ACCEPT_LANGUAGE='en'
) )
...@@ -83,7 +83,7 @@ class InternationalizationTest(ModuleStoreTestCase): ...@@ -83,7 +83,7 @@ class InternationalizationTest(ModuleStoreTestCase):
self.client.login(username=self.uname, password=self.password) self.client.login(username=self.uname, password=self.password)
resp = self.client.get_html( resp = self.client.get_html(
'/course', '/course/',
{}, {},
HTTP_ACCEPT_LANGUAGE='eo' HTTP_ACCEPT_LANGUAGE='eo'
) )
......
...@@ -15,15 +15,13 @@ from django.contrib.auth.models import User ...@@ -15,15 +15,13 @@ from django.contrib.auth.models import User
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.course_module import CourseDescriptor
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from uuid import uuid4 from uuid import uuid4
from pymongo import MongoClient from pymongo import MongoClient
...@@ -72,26 +70,26 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -72,26 +70,26 @@ class ContentStoreImportTest(ModuleStoreTestCase):
content_store = contentstore() content_store = contentstore()
module_store = modulestore('direct') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True) import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True)
course_location = CourseDescriptor.id_to_location('edX/test_import_course/2012_Fall') course_id = SlashSeparatedCourseKey('edX', 'test_import_course', '2012_Fall')
course = module_store.get_item(course_location) course = module_store.get_course(course_id)
self.assertIsNotNone(course) self.assertIsNotNone(course)
return module_store, content_store, course, course_location return module_store, content_store, course
def test_unicode_chars_in_course_name_import(self): def test_unicode_chars_in_course_name_import(self):
""" """
# Test that importing course with unicode 'id' and 'display name' doesn't give UnicodeEncodeError # Test that importing course with unicode 'id' and 'display name' doesn't give UnicodeEncodeError
""" """
module_store = modulestore('direct') 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( import_from_xml(
module_store, module_store,
'common/test/data/', 'common/test/data/',
['2014_Uni'], ['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) self.assertIsNotNone(course)
# test that course 'display_name' same as imported course 'display_name' # test that course 'display_name' same as imported course 'display_name'
...@@ -101,17 +99,19 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -101,17 +99,19 @@ class ContentStoreImportTest(ModuleStoreTestCase):
''' '''
Stuff in static_import should always be imported into contentstore 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") # 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) print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 1) self.assertEqual(len(all_assets), 1)
self.assertEqual(count, 1) self.assertEqual(count, 1)
content = None content = None
try: 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) content = content_store.find(location)
except NotFoundError: except NotFoundError:
pass pass
...@@ -131,92 +131,93 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -131,92 +131,93 @@ class ContentStoreImportTest(ModuleStoreTestCase):
module_store = modulestore('direct') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True) 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') course = module_store.get_course(SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'))
module_store.get_item(course_location)
# make sure we have NO assets in our contentstore # 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(len(all_assets), 0)
self.assertEqual(count, 0) self.assertEqual(count, 0)
def test_no_static_link_rewrites_on_import(self): def test_no_static_link_rewrites_on_import(self):
module_store = modulestore('direct') 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) 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) self.assertIn('/static/', handouts.data)
def test_tab_name_imports_correctly(self): 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) print "course tabs = {0}".format(course.tabs)
self.assertEqual(course.tabs[2]['name'], 'Syllabus') self.assertEqual(course.tabs[2]['name'], 'Syllabus')
def test_rewrite_reference_list(self): def test_rewrite_reference_list(self):
module_store = modulestore('direct') 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( import_from_xml(
module_store, module_store,
'common/test/data/', 'common/test/data/',
['conditional'], ['conditional'],
target_location_namespace=target_location target_course_id=target_course_id
) )
conditional_module = module_store.get_item( 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) self.assertIsNotNone(conditional_module)
different_course_id = SlashSeparatedCourseKey('edX', 'different_course', 'copy_run')
self.assertListEqual( self.assertListEqual(
[ [
u'i4x://testX/conditional_copy/problem/choiceprob', target_course_id.make_usage_key('problem', 'choiceprob'),
u'i4x://edX/different_course/html/for_testing_import_rewrites' different_course_id.make_usage_key('html', 'for_testing_import_rewrites')
], ],
conditional_module.sources_list conditional_module.sources_list
) )
self.assertListEqual( self.assertListEqual(
[ [
u'i4x://testX/conditional_copy/html/congrats', target_course_id.make_usage_key('html', 'congrats'),
u'i4x://testX/conditional_copy/html/secret_page' target_course_id.make_usage_key('html', 'secret_page')
], ],
conditional_module.show_tag_list conditional_module.show_tag_list
) )
def test_rewrite_reference(self): def test_rewrite_reference(self):
module_store = modulestore('direct') 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( import_from_xml(
module_store, module_store,
'common/test/data/', 'common/test/data/',
['open_ended'], ['open_ended'],
target_location_namespace=target_location target_course_id=target_course_id
) )
peergrading_module = module_store.get_item( 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.assertIsNotNone(peergrading_module)
self.assertEqual( self.assertEqual(
u'i4x://testX/peergrading_copy/combinedopenended/SampleQuestion', target_course_id.make_usage_key('combinedopenended', 'SampleQuestion'),
peergrading_module.link_to_location peergrading_module.link_to_location
) )
def test_rewrite_reference_value_dict(self): def test_rewrite_reference_value_dict(self):
module_store = modulestore('direct') 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( import_from_xml(
module_store, module_store,
'common/test/data/', 'common/test/data/',
['split_test_module'], ['split_test_module'],
target_location_namespace=target_location target_course_id=target_course_id
) )
split_test_module = module_store.get_item( 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.assertIsNotNone(split_test_module)
self.assertEqual( self.assertEqual(
{ {
"0": "i4x://testX/split_test_copy/vertical/sample_0", "0": target_course_id.make_usage_key('vertical', 'sample_0'),
"2": "i4x://testX/split_test_copy/vertical/sample_2", "2": target_course_id.make_usage_key('vertical', 'sample_2'),
}, },
split_test_module.group_id_to_child, split_test_module.group_id_to_child,
) )
...@@ -4,7 +4,6 @@ from xmodule.modulestore.xml_importer import import_from_xml ...@@ -4,7 +4,6 @@ from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
...@@ -17,10 +16,9 @@ class DraftReorderTestCase(ModuleStoreTestCase): ...@@ -17,10 +16,9 @@ class DraftReorderTestCase(ModuleStoreTestCase):
def test_order(self): def test_order(self):
store = modulestore('direct') store = modulestore('direct')
draft_store = modulestore('default') draft_store = modulestore('default')
import_from_xml(store, 'common/test/data/', ['import_draft_order'], draft_store=draft_store) _, course_items = import_from_xml(store, 'common/test/data/', ['import_draft_order'], draft_store=draft_store)
sequential = draft_store.get_item( course_key = course_items[0].id
Location('i4x', 'test_org', 'import_draft_order', 'sequential', '0f4f7649b10141b0bdc9922dcf94515a', None) sequential = draft_store.get_item(course_key.make_usage_key('sequential', '0f4f7649b10141b0bdc9922dcf94515a'))
)
verticals = sequential.children verticals = sequential.children
# The order that files are read in from the file system is not guaranteed (cannot rely on # The order that files are read in from the file system is not guaranteed (cannot rely on
...@@ -32,22 +30,20 @@ class DraftReorderTestCase(ModuleStoreTestCase): ...@@ -32,22 +30,20 @@ class DraftReorderTestCase(ModuleStoreTestCase):
# #
# '5a05be9d59fc4bb79282c94c9e6b88c7' and 'second' are public verticals. # '5a05be9d59fc4bb79282c94c9e6b88c7' and 'second' are public verticals.
self.assertEqual(7, len(verticals)) self.assertEqual(7, len(verticals))
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/z', verticals[0]) self.assertEqual(course_key.make_usage_key('vertical', 'z'), verticals[0])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/5a05be9d59fc4bb79282c94c9e6b88c7', verticals[1]) self.assertEqual(course_key.make_usage_key('vertical', '5a05be9d59fc4bb79282c94c9e6b88c7'), verticals[1])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/a', verticals[2]) self.assertEqual(course_key.make_usage_key('vertical', 'a'), verticals[2])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/second', verticals[3]) self.assertEqual(course_key.make_usage_key('vertical', 'second'), verticals[3])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/b', verticals[4]) self.assertEqual(course_key.make_usage_key('vertical', 'b'), verticals[4])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/d', verticals[5]) self.assertEqual(course_key.make_usage_key('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', 'c'), verticals[6])
# Now also test that the verticals in a second sequential are correct. # Now also test that the verticals in a second sequential are correct.
sequential = draft_store.get_item( sequential = draft_store.get_item(course_key.make_usage_key('sequential', 'secondseq'))
Location('i4x', 'test_org', 'import_draft_order', 'sequential', 'secondseq', None)
)
verticals = sequential.children verticals = sequential.children
# 'asecond' and 'zsecond' are drafts with 'index_in_children_list' 0 and 2, respectively. # 'asecond' and 'zsecond' are drafts with 'index_in_children_list' 0 and 2, respectively.
# 'secondsubsection' is a public vertical. # 'secondsubsection' is a public vertical.
self.assertEqual(3, len(verticals)) self.assertEqual(3, len(verticals))
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/asecond', verticals[0]) self.assertEqual(course_key.make_usage_key('vertical', 'asecond'), verticals[0])
self.assertEqual(u'i4x://test_org/import_draft_order/vertical/secondsubsection', verticals[1]) self.assertEqual(course_key.make_usage_key('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', 'zsecond'), verticals[2])
...@@ -10,6 +10,7 @@ from xblock.fields import String ...@@ -10,6 +10,7 @@ from xblock.fields import String
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo.draft import as_draft
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
...@@ -39,7 +40,6 @@ class XBlockImportTest(ModuleStoreTestCase): ...@@ -39,7 +40,6 @@ class XBlockImportTest(ModuleStoreTestCase):
def test_import_public(self): def test_import_public(self):
self._assert_import( self._assert_import(
'pure_xblock_public', 'pure_xblock_public',
'i4x://edX/pure_xblock_public/stubxblock/xblock_test',
'set by xml' 'set by xml'
) )
...@@ -47,12 +47,11 @@ class XBlockImportTest(ModuleStoreTestCase): ...@@ -47,12 +47,11 @@ class XBlockImportTest(ModuleStoreTestCase):
def test_import_draft(self): def test_import_draft(self):
self._assert_import( self._assert_import(
'pure_xblock_draft', 'pure_xblock_draft',
'i4x://edX/pure_xblock_draft/stubxblock/xblock_test@draft',
'set by xml', 'set by xml',
has_draft=True 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 Import a course from XML, then verify that the XBlock was loaded
with the correct field value. with the correct field value.
...@@ -67,16 +66,21 @@ class XBlockImportTest(ModuleStoreTestCase): ...@@ -67,16 +66,21 @@ class XBlockImportTest(ModuleStoreTestCase):
the expected field value set. the expected field value set.
""" """
import_from_xml( _, courses = import_from_xml(
self.store, 'common/test/data', [course_dir], self.store, 'common/test/data', [course_dir],
draft_store=self.draft_store 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.assertTrue(isinstance(xblock, StubXBlock))
self.assertEqual(xblock.test_field, expected_field_val) self.assertEqual(xblock.test_field, expected_field_val)
if has_draft: 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.assertTrue(isinstance(draft_xblock, StubXBlock))
self.assertEqual(draft_xblock.test_field, expected_field_val) self.assertEqual(draft_xblock.test_field, expected_field_val)
...@@ -3,9 +3,10 @@ Test finding orphans via the view and django config ...@@ -3,9 +3,10 @@ Test finding orphans via the view and django config
""" """
import json import json
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.django import loc_mapper
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url
class TestOrphan(CourseTestCase): class TestOrphan(CourseTestCase):
""" """
...@@ -27,6 +28,8 @@ class TestOrphan(CourseTestCase): ...@@ -27,6 +28,8 @@ class TestOrphan(CourseTestCase):
self._create_item('about', 'overview', "<p>overview</p>", {}, None, None, runtime) 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._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): def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
location = self.course.location.replace(category=category, name=name) location = self.course.location.replace(category=category, name=name)
store = modulestore('direct') store = modulestore('direct')
...@@ -35,39 +38,34 @@ class TestOrphan(CourseTestCase): ...@@ -35,39 +38,34 @@ class TestOrphan(CourseTestCase):
# add child to parent in mongo # add child to parent in mongo
parent_location = self.course.location.replace(category=parent_category, name=parent_name) parent_location = self.course.location.replace(category=parent_category, name=parent_name)
parent = store.get_item(parent_location) parent = store.get_item(parent_location)
parent.children.append(location.url()) parent.children.append(location)
store.update_item(parent, self.user.id) store.update_item(parent, self.user.id)
def test_mongo_orphan(self): def test_mongo_orphan(self):
""" """
Test that old mongo finds the orphans 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( orphans = json.loads(
self.client.get( self.client.get(
orphan_url, self.orphan_url,
HTTP_ACCEPT='application/json' HTTP_ACCEPT='application/json'
).content ).content
) )
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans)) self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
location = self.course.location.replace(category='chapter', name='OrphanChapter') 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') 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') 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): def test_mongo_orphan_delete(self):
""" """
Test that old mongo deletes the orphans Test that old mongo deletes the orphans
""" """
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.client.delete(self.orphan_url)
orphan_url = locator.url_reverse('orphan/', '')
self.client.delete(orphan_url)
orphans = json.loads( 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)) self.assertEqual(len(orphans), 0, "Orphans not deleted {}".format(orphans))
...@@ -76,10 +74,8 @@ class TestOrphan(CourseTestCase): ...@@ -76,10 +74,8 @@ class TestOrphan(CourseTestCase):
Test that auth restricts get and delete appropriately Test that auth restricts get and delete appropriately
""" """
test_user_client, test_user = self.create_non_staff_authed_user_client() test_user_client, test_user = self.create_non_staff_authed_user_client()
CourseEnrollment.enroll(test_user, self.course.location.course_id) CourseEnrollment.enroll(test_user, self.course.id)
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) response = test_user_client.get(self.orphan_url)
orphan_url = locator.url_reverse('orphan/', '')
response = test_user_client.get(orphan_url)
self.assertEqual(response.status_code, 403) 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) self.assertEqual(response.status_code, 403)
...@@ -4,13 +4,13 @@ Test CRUD for authorization. ...@@ -4,13 +4,13 @@ Test CRUD for authorization.
import copy import copy
from django.test.utils import override_settings 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 xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.tests.utils import AjaxEnabledTestClient
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.locations import SlashSeparatedCourseKey
from xmodule.modulestore import Location from contentstore.utils import reverse_url, reverse_course_url
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
from contentstore.views.access import has_course_access from contentstore.views.access import has_course_access
from student import auth from student import auth
...@@ -46,17 +46,14 @@ class TestCourseAccess(ModuleStoreTestCase): ...@@ -46,17 +46,14 @@ class TestCourseAccess(ModuleStoreTestCase):
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
# create a course via the view handler which has a different strategy for permissions than the factory # 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_key = SlashSeparatedCourseKey('myu', 'mydept.mycourse', 'myrun')
self.course_locator = loc_mapper().translate_location( course_url = reverse_url('course_handler')
self.course_location.course_id, self.course_location, False, True self.client.ajax_post(course_url,
)
self.client.ajax_post(
self.course_locator.url_reverse('course'),
{ {
'org': self.course_location.org, 'org': self.course_key.org,
'number': self.course_location.course, 'number': self.course_key.course,
'display_name': 'My favorite course', 'display_name': 'My favorite course',
'run': self.course_location.name, 'run': self.course_key.run,
} }
) )
...@@ -91,7 +88,7 @@ class TestCourseAccess(ModuleStoreTestCase): ...@@ -91,7 +88,7 @@ class TestCourseAccess(ModuleStoreTestCase):
# first check the course creator.has explicit access (don't use has_access as is_staff # first check the course creator.has explicit access (don't use has_access as is_staff
# will trump the actual test) # will trump the actual test)
self.assertTrue( self.assertTrue(
CourseInstructorRole(self.course_locator).has_user(self.user), CourseInstructorRole(self.course_key).has_user(self.user),
"Didn't add creator as instructor." "Didn't add creator as instructor."
) )
users = copy.copy(self.users) users = copy.copy(self.users)
...@@ -101,35 +98,28 @@ class TestCourseAccess(ModuleStoreTestCase): ...@@ -101,35 +98,28 @@ class TestCourseAccess(ModuleStoreTestCase):
for role in [CourseInstructorRole, CourseStaffRole]: for role in [CourseInstructorRole, CourseStaffRole]:
user_by_role[role] = [] user_by_role[role] = []
# pylint: disable=protected-access # pylint: disable=protected-access
groupnames = role(self.course_locator)._group_names group = role(self.course_key)
self.assertGreater(len(groupnames), 1, "Only 0 or 1 groupname for {}".format(role.ROLE))
# NOTE: this loop breaks the roles.py abstraction by purposely assigning # 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 # users to one of each possible groupname in order to test that has_course_access
# and remove_user work # and remove_user work
for groupname in groupnames: user = users.pop()
group, _ = Group.objects.get_or_create(name=groupname) group.add_users(user)
user = users.pop() user_by_role[role].append(user)
user_by_role[role].append(user) self.assertTrue(has_course_access(user, self.course_key), "{} does not have access".format(user))
user.groups.add(group)
user.save() course_team_url = reverse_course_url('course_team_handler', self.course_key)
self.assertTrue(has_course_access(user, self.course_locator), "{} does not have access".format(user)) response = self.client.get_html(course_team_url)
self.assertTrue(has_course_access(user, self.course_location), "{} does not have access".format(user))
response = self.client.get_html(self.course_locator.url_reverse('course_team'))
for role in [CourseInstructorRole, CourseStaffRole]: for role in [CourseInstructorRole, CourseStaffRole]:
for user in user_by_role[role]: for user in user_by_role[role]:
self.assertContains(response, user.email) self.assertContains(response, user.email)
# test copying course permissions # test copying course permissions
copy_course_location = Location(['i4x', 'copyu', 'copydept.mycourse', 'course', 'myrun']) copy_course_key = SlashSeparatedCourseKey('copyu', 'copydept.mycourse', 'myrun')
copy_course_locator = loc_mapper().translate_location(
copy_course_location.course_id, copy_course_location, False, True
)
for role in [CourseInstructorRole, CourseStaffRole]: for role in [CourseInstructorRole, CourseStaffRole]:
auth.add_users( auth.add_users(
self.user, self.user,
role(copy_course_locator), role(copy_course_key),
*role(self.course_locator).users_with_role() *role(self.course_key).users_with_role()
) )
# verify access in copy course and verify that removal from source course w/ the various # verify access in copy course and verify that removal from source course w/ the various
# groupnames works # groupnames works
...@@ -138,10 +128,9 @@ class TestCourseAccess(ModuleStoreTestCase): ...@@ -138,10 +128,9 @@ class TestCourseAccess(ModuleStoreTestCase):
# forcefully decache the groups: premise is that any real request will not have # 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 # multiple objects repr the same user but this test somehow uses different instance
# in above add_users call # in above add_users call
if hasattr(user, '_groups'): if hasattr(user, '_roles'):
del user._groups 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_key), "{} 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_key), user)
auth.remove_users(self.user, role(self.course_locator), user) self.assertFalse(has_course_access(user, self.course_key), "{} remove didn't work".format(user))
self.assertFalse(has_course_access(user, self.course_locator), "{} remove didn't work".format(user))
...@@ -111,18 +111,14 @@ class TestSaveSubsToStore(ModuleStoreTestCase): ...@@ -111,18 +111,14 @@ class TestSaveSubsToStore(ModuleStoreTestCase):
self.subs_id = str(uuid4()) self.subs_id = str(uuid4())
filename = 'subs_{0}.srt.sjson'.format(self.subs_id) filename = 'subs_{0}.srt.sjson'.format(self.subs_id)
self.content_location = StaticContent.compute_location( self.content_location = StaticContent.compute_location(self.course.id, filename)
self.org, self.number, filename
)
# incorrect subs # incorrect subs
self.unjsonable_subs = set([1]) # set can't be serialized self.unjsonable_subs = set([1]) # set can't be serialized
self.unjsonable_subs_id = str(uuid4()) self.unjsonable_subs_id = str(uuid4())
filename_unjsonable = 'subs_{0}.srt.sjson'.format(self.unjsonable_subs_id) filename_unjsonable = 'subs_{0}.srt.sjson'.format(self.unjsonable_subs_id)
self.content_location_unjsonable = StaticContent.compute_location( self.content_location_unjsonable = StaticContent.compute_location(self.course.id, filename_unjsonable)
self.org, self.number, filename_unjsonable
)
self.clear_subs_content() self.clear_subs_content()
...@@ -172,9 +168,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -172,9 +168,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
"""Remove, if subtitles content exists.""" """Remove, if subtitles content exists."""
for subs_id in youtube_subs.values(): for subs_id in youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(self.course.id, filename)
self.org, self.number, filename
)
try: try:
content = contentstore().find(content_location) content = contentstore().find(content_location)
contentstore().delete(content.get_id()) contentstore().delete(content.get_id())
...@@ -218,9 +212,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -218,9 +212,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
# Check assets status after importing subtitles. # Check assets status after importing subtitles.
for subs_id in good_youtube_subs.values(): for subs_id in good_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(self.course.id, filename)
self.org, self.number, filename
)
self.assertTrue(contentstore().find(content_location)) self.assertTrue(contentstore().find(content_location))
self.clear_subs_content(good_youtube_subs) self.clear_subs_content(good_youtube_subs)
...@@ -256,7 +248,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -256,7 +248,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
for subs_id in bad_youtube_subs.values(): for subs_id in bad_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(
self.org, self.number, filename self.course.id, filename
) )
with self.assertRaises(NotFoundError): with self.assertRaises(NotFoundError):
contentstore().find(content_location) contentstore().find(content_location)
...@@ -282,7 +274,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase): ...@@ -282,7 +274,7 @@ class TestDownloadYoutubeSubs(ModuleStoreTestCase):
for subs_id in good_youtube_subs.values(): for subs_id in good_youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(
self.org, self.number, filename self.course.id, filename
) )
self.assertTrue(contentstore().find(content_location)) self.assertTrue(contentstore().find(content_location))
...@@ -317,7 +309,7 @@ class TestGenerateSubsFromSource(TestDownloadYoutubeSubs): ...@@ -317,7 +309,7 @@ class TestGenerateSubsFromSource(TestDownloadYoutubeSubs):
for subs_id in youtube_subs.values(): for subs_id in youtube_subs.values():
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(
self.org, self.number, filename self.course.id, filename
) )
self.assertTrue(contentstore().find(content_location)) 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 ...@@ -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 after deleting it creates same course again
""" """
from contentstore.tests.utils import AjaxEnabledTestClient 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 courseware.tests.factories import UserFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import loc_mapper
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.locations import SlashSeparatedCourseKey
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -27,23 +27,20 @@ class TestUsersDefaultRole(ModuleStoreTestCase): ...@@ -27,23 +27,20 @@ class TestUsersDefaultRole(ModuleStoreTestCase):
self.client.login(username=self.user.username, password='test') self.client.login(username=self.user.username, password='test')
# create a course via the view handler to create course # create a course via the view handler to create course
self.course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1']) self.course_key = SlashSeparatedCourseKey('Org_1', 'Course_1', 'Run_1')
self._create_course_with_given_location(self.course_location) 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 Create course at provided location
""" """
course_locator = loc_mapper().translate_location(
course_location.course_id, course_location, False, True
)
resp = self.client.ajax_post( resp = self.client.ajax_post(
course_locator.url_reverse('course'), reverse_url('course_handler'),
{ {
'org': course_location.org, 'org': course_key.org,
'number': course_location.course, 'number': course_key.course,
'display_name': 'test course', 'display_name': 'test course',
'run': course_location.name, 'run': course_key.run,
} }
) )
return resp return resp
...@@ -60,66 +57,61 @@ class TestUsersDefaultRole(ModuleStoreTestCase): ...@@ -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 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 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 # 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 # 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 # 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 # 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): def test_user_role_on_course_recreate(self):
""" """
Test that creating same course again after deleting it gives user his default Test that creating same course again after deleting it gives user his default
forum role "Student" for that course 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 # 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(CourseEnrollment.is_enrolled(self.user, self.course_key))
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 this course and recreate this course with same user # delete this course and recreate this course with same user
delete_course_and_groups(course_id, commit=True) delete_course_and_groups(self.course_key, commit=True)
resp = self._create_course_with_given_location(self.course_location) resp = self._create_course_with_given_location(self.course_key)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# check that user has his enrollment for this course # 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 # 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): 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 Test that creating same course again with different name case after deleting it gives user
his default forum role "Student" for that course 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 # 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 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') # now create same course with different name case ('uppercase')
new_course_location = Location( new_course_key = self.course_key.replace(course=self.course_key.course.upper())
['i4x', course_location.org, course_location.course.upper(), 'course', course_location.name] resp = self._create_course_with_given_location(new_course_key)
)
resp = self._create_course_with_given_location(new_course_location)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# check that user has his default "Student" forum role again for this course (with changed name case) # check that user has his default "Student" forum role again for this course (with changed name case)
self.assertTrue( 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) # Disabled due to case-sensitive test db (sqlite3)
# # check that there user has only one "Student" forum role (with new updated course_id) # # 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').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 ...@@ -7,8 +7,8 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from contentstore import utils from contentstore import utils
from xmodule.modulestore import Location
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class LMSLinksTestCase(TestCase): class LMSLinksTestCase(TestCase):
...@@ -57,29 +57,27 @@ class LMSLinksTestCase(TestCase): ...@@ -57,29 +57,27 @@ class LMSLinksTestCase(TestCase):
def get_about_page_link(self): def get_about_page_link(self):
""" create mock course and return the about page link """ """ create mock course and return the about page link """
location = Location('i4x', 'mitX', '101', 'course', 'test') course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
return utils.get_lms_link_for_about_page(location) return utils.get_lms_link_for_about_page(course_key)
def lms_link_test(self): def lms_link_test(self):
""" Tests get_lms_link_for_item. """ """ Tests get_lms_link_for_item. """
location = Location('i4x', 'mitX', '101', 'vertical', 'contacting_us') course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
link = utils.get_lms_link_for_item(location, False, "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") 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( self.assertEquals(
link, link,
"//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us" "//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 # now test with the course' location
# Studio dashboard. location = course_key.make_usage_key('course', 'test')
location = Location('i4x', 'mitX', '101', 'course', 'test')
link = utils.get_lms_link_for_item(location) link = utils.get_lms_link_for_item(location)
self.assertEquals( self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")
link,
"//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test"
)
class ExtraPanelTabTestCase(TestCase): class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """ """ Tests adding and removing extra course tabs. """
......
...@@ -7,9 +7,9 @@ import unittest ...@@ -7,9 +7,9 @@ import unittest
from django.test.utils import override_settings from django.test.utils import override_settings
from django.core.cache import cache from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -235,13 +235,13 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -235,13 +235,13 @@ class AuthTestCase(ContentStoreTestCase):
def test_private_pages_auth(self): def test_private_pages_auth(self):
"""Make sure pages that do require login work.""" """Make sure pages that do require login work."""
auth_pages = ( auth_pages = (
'/course', '/course/',
) )
# These are pages that should just load when the user is logged in # These are pages that should just load when the user is logged in
# (no data needed) # (no data needed)
simple_auth_pages = ( simple_auth_pages = (
'/course', '/course/',
) )
# need an activated user # need an activated user
...@@ -267,7 +267,7 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -267,7 +267,7 @@ class AuthTestCase(ContentStoreTestCase):
def test_index_auth(self): def test_index_auth(self):
# not logged in. Should return a redirect. # 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) self.assertEqual(resp.status_code, 302)
# Logged in should work. # Logged in should work.
...@@ -284,16 +284,17 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -284,16 +284,17 @@ class AuthTestCase(ContentStoreTestCase):
self.login(self.email, self.pw) self.login(self.email, self.pw)
# make sure we can access courseware immediately # 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) self.assertEquals(resp.status_code, 200)
# then wait a bit and see if we get timed out # then wait a bit and see if we get timed out
time.sleep(2) 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 # 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): class ForumTestCase(CourseTestCase):
......
...@@ -13,7 +13,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -13,7 +13,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE from contentstore.tests.modulestore_config import TEST_MODULESTORE
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from xmodule.modulestore.django import loc_mapper
def parse_json(response): def parse_json(response):
...@@ -92,10 +91,6 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -92,10 +91,6 @@ class CourseTestCase(ModuleStoreTestCase):
number='999', number='999',
display_name='Robot Super Course', 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) self.store = get_modulestore(self.course.location)
def create_non_staff_authed_user_client(self): def create_non_staff_authed_user_client(self):
...@@ -133,7 +128,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -133,7 +128,7 @@ class CourseTestCase(ModuleStoreTestCase):
""" """
Reloads the course object from the database 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): def save_course(self):
""" """
......
from ..utils import get_course_location_for_item
from xmodule.modulestore.locator import CourseLocator
from student.roles import CourseStaffRole, GlobalStaff, CourseInstructorRole from student.roles import CourseStaffRole, GlobalStaff, CourseInstructorRole
from student import auth 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 Note that the CMS permissions model is with respect to courses
There is a super-admin permissions if user.is_staff is set There is a super-admin permissions if user.is_staff is set
Also, since we're unifying the user database between LMS and CAS, Also, since we're unifying the user database between LMS and CAS,
...@@ -16,21 +14,22 @@ def has_course_access(user, location, role=CourseStaffRole): ...@@ -16,21 +14,22 @@ def has_course_access(user, location, role=CourseStaffRole):
""" """
if GlobalStaff().has_user(user): if GlobalStaff().has_user(user):
return True return True
if not isinstance(location, CourseLocator): return auth.has_access(user, role(course_key))
# this can be expensive if location is not category=='course'
location = get_course_location_for_item(location)
return auth.has_access(user, role(location))
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. 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 course_id: the course_id of the course we're interested in
:param context: a course_id. This is not used if location is a CourseLocator.
""" """
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' return 'instructor'
else: else:
return 'staff' return 'staff'
...@@ -9,12 +9,12 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -9,12 +9,12 @@ from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.core.exceptions import PermissionDenied 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 .access import has_course_access
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.locator import BlockUsageLocator
__all__ = ['checklists_handler'] __all__ = ['checklists_handler']
...@@ -23,7 +23,7 @@ __all__ = ['checklists_handler'] ...@@ -23,7 +23,7 @@ __all__ = ['checklists_handler']
@require_http_methods(("GET", "POST", "PUT")) @require_http_methods(("GET", "POST", "PUT"))
@login_required @login_required
@ensure_csrf_cookie @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. The restful handler for checklists.
...@@ -33,14 +33,11 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -33,14 +33,11 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
POST or PUT POST or PUT
json: updates the checked state for items within a particular checklist. checklist_index is required. 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) course_key = CourseKey.from_string(course_key_string)
if not has_course_access(request.user, location): if not has_course_access(request.user, course_key):
raise PermissionDenied() raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location) course_module = modulestore().get_course(course_key)
modulestore = get_modulestore(old_location)
course_module = modulestore.get_item(old_location)
json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json') json_request = 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
if request.method == 'GET': if request.method == 'GET':
...@@ -48,13 +45,13 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -48,13 +45,13 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
# from the template. # from the template.
if not course_module.checklists: if not course_module.checklists:
course_module.checklists = CourseDescriptor.checklists.default 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) expanded_checklists = expand_all_action_urls(course_module)
if json_request: if json_request:
return JsonResponse(expanded_checklists) return JsonResponse(expanded_checklists)
else: else:
handler_url = location.url_reverse('checklists/', '') handler_url = reverse_course_url('checklists_handler', course_key)
return render_to_response('checklists.html', return render_to_response('checklists.html',
{ {
'handler_url': handler_url, 'handler_url': handler_url,
...@@ -77,7 +74,7 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -77,7 +74,7 @@ def checklists_handler(request, tag=None, package_id=None, branch=None, version_
# not default # not default
course_module.checklists = course_module.checklists course_module.checklists = course_module.checklists
course_module.save() 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) expanded_checklist = expand_checklist_action_url(course_module, persisted_checklist)
return JsonResponse(expanded_checklist) return JsonResponse(expanded_checklist)
else: else:
...@@ -112,18 +109,15 @@ def expand_checklist_action_url(course_module, checklist): ...@@ -112,18 +109,15 @@ def expand_checklist_action_url(course_module, checklist):
expanded_checklist = copy.deepcopy(checklist) expanded_checklist = copy.deepcopy(checklist)
urlconf_map = { urlconf_map = {
"ManageUsers": "course_team", "ManageUsers": "course_team_handler",
"CourseOutline": "course", "CourseOutline": "course_handler",
"SettingsDetails": "settings/details", "SettingsDetails": "settings_handler",
"SettingsGrading": "settings/grading", "SettingsGrading": "grading_handler",
} }
for item in expanded_checklist.get('items'): for item in expanded_checklist.get('items'):
action_url = item.get('action_url') action_url = item.get('action_url')
if action_url in urlconf_map: if action_url in urlconf_map:
url_prefix = urlconf_map[action_url] item['action_url'] = reverse_course_url(urlconf_map[action_url], course_module.id)
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, '')
return expanded_checklist return expanded_checklist
...@@ -14,8 +14,6 @@ from edxmako.shortcuts import render_to_response ...@@ -14,8 +14,6 @@ from edxmako.shortcuts import render_to_response
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from xmodule.modulestore.django import modulestore 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.core import XBlock
from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.django.request import webob_to_django_response, django_to_webob_request
...@@ -24,12 +22,11 @@ from xblock.fields import Scope ...@@ -24,12 +22,11 @@ from xblock.fields import Scope
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist 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.utils import get_lms_link_for_item, compute_publish_state, PublishState, get_modulestore
from contentstore.views.helpers import get_parent_xblock from contentstore.views.helpers import get_parent_xblock
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from xmodule.modulestore.keys import UsageKey
from .access import has_course_access from .access import has_course_access
...@@ -70,7 +67,7 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ...@@ -70,7 +67,7 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
@require_GET @require_GET
@login_required @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. The restful handler for subsection-specific requests.
...@@ -79,13 +76,13 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -79,13 +76,13 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
json: not currently supported json: not currently supported
""" """
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): 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: 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: except ItemNotFoundError:
return HttpResponseBadRequest() 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 # make sure that location references a 'sequential', otherwise return
# BadRequest # BadRequest
...@@ -114,10 +111,6 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -114,10 +111,6 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
can_view_live = True can_view_live = True
break break
course_locator = loc_mapper().translate_location(
course.location.course_id, course.location, False, True
)
return render_to_response( return render_to_response(
'edit_subsection.html', 'edit_subsection.html',
{ {
...@@ -126,9 +119,9 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -126,9 +119,9 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'lms_link': lms_link, 'lms_link': lms_link,
'preview_link': preview_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, 'parent_item': parent,
'locator': locator, 'locator': usage_key,
'policy_metadata': policy_metadata, 'policy_metadata': policy_metadata,
'subsection_units': subsection_units, 'subsection_units': subsection_units,
'can_view_live': can_view_live 'can_view_live': can_view_live
...@@ -149,7 +142,7 @@ def _load_mixed_class(category): ...@@ -149,7 +142,7 @@ def _load_mixed_class(category):
@require_GET @require_GET
@login_required @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. The restful handler for unit-specific requests.
...@@ -158,9 +151,9 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -158,9 +151,9 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
json: not currently supported json: not currently supported
""" """
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): 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: 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: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
...@@ -230,12 +223,6 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -230,12 +223,6 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
) )
xblocks = item.get_children() 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, # TODO (cpennington): If we share units between courses,
# this will need to change to check permissions correctly so as # this will need to change to check permissions correctly so as
...@@ -272,8 +259,8 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -272,8 +259,8 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
return render_to_response('unit.html', { return render_to_response('unit.html', {
'context_course': course, 'context_course': course,
'unit': item, 'unit': item,
'unit_locator': locator, 'unit_locator': usage_key,
'locators': locators, 'xblocks': xblocks,
'component_templates': component_templates, 'component_templates': component_templates,
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
...@@ -297,7 +284,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -297,7 +284,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
# pylint: disable=unused-argument # pylint: disable=unused-argument
@require_GET @require_GET
@login_required @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. The restful handler for container xblock requests.
...@@ -306,9 +293,11 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g ...@@ -306,9 +293,11 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
json: not currently supported json: not currently supported
""" """
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): 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)
if not has_course_access(request.user, usage_key.course_key):
raise PermissionDenied()
try: try:
__, course, xblock, __ = _get_item_in_course(request, locator) xblock = get_modulestore(usage_key).get_item(usage_key)
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
...@@ -323,11 +312,10 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g ...@@ -323,11 +312,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 unit_publish_state = compute_publish_state(unit) if unit else None
return render_to_response('container.html', { return render_to_response('container.html', {
'context_course': course,
'xblock': xblock, 'xblock': xblock,
'xblock_locator': locator,
'unit': unit,
'unit_publish_state': unit_publish_state, 'unit_publish_state': unit_publish_state,
'xblock_locator': usage_key,
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
'ancestor_xblocks': ancestor_xblocks, 'ancestor_xblocks': ancestor_xblocks,
}) })
else: else:
...@@ -335,32 +323,32 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g ...@@ -335,32 +323,32 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
@login_required @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, Helper method for getting the old location, containing course,
item, and lms_link for a given locator. item, and lms_link for a given locator.
Verifies that the caller has permission to access this item. 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() raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(locator) course = modulestore().get_course(course_key)
course_location = loc_mapper().translate_locator_to_location(locator, True) item = get_modulestore(usage_key).get_item(usage_key, depth=1)
course = modulestore().get_item(course_location) lms_link = get_lms_link_for_item(usage_key)
item = modulestore().get_item(old_location, depth=1)
lms_link = get_lms_link_for_item(old_location, course_id=course.location.course_id)
return old_location, course, item, lms_link return course, item, lms_link
@login_required @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 Dispatch an AJAX action to an xblock
Args: 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 handler (str): The handler to execute
suffix (str): The remainder of the url to be passed to the handler suffix (str): The remainder of the url to be passed to the handler
...@@ -369,9 +357,9 @@ def component_handler(request, usage_id, handler, suffix=''): ...@@ -369,9 +357,9 @@ def component_handler(request, usage_id, handler, suffix=''):
django response 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 # Let the module handle the AJAX
req = django_to_webob_request(request) req = django_to_webob_request(request)
...@@ -384,6 +372,6 @@ def component_handler(request, usage_id, handler, suffix=''): ...@@ -384,6 +372,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 # unintentional update to handle any side effects of handle call; so, request user didn't author
# the change # the change
get_modulestore(location).update_item(descriptor, None) get_modulestore(usage_key).update_item(descriptor, None)
return webob_to_django_response(resp) return webob_to_django_response(resp)
...@@ -13,22 +13,23 @@ from django.utils.translation import ugettext as _ ...@@ -13,22 +13,23 @@ from django.utils.translation import ugettext as _
from .access import has_course_access from .access import has_course_access
import contentstore.git_export_utils as git_export_utils import contentstore.git_export_utils as git_export_utils
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.keys import CourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @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 This method serves up the 'Export to Git' page
""" """
location = Location('i4x', org, course, 'course', name) course_key = CourseKey.from_string(course_key_string)
if not has_course_access(request.user, location): if not has_course_access(request.user, course_key):
raise PermissionDenied() raise PermissionDenied()
course_module = modulestore().get_item(location)
course_module = modulestore().get_course(course_key)
failed = False failed = False
log.debug('export_git course_module=%s', course_module) log.debug('export_git course_module=%s', course_module)
......
...@@ -3,7 +3,8 @@ import logging ...@@ -3,7 +3,8 @@ import logging
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from edxmako.shortcuts import render_to_string, render_to_response 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'] __all__ = ['edge', 'event', 'landing']
...@@ -48,7 +49,7 @@ def get_parent_xblock(xblock): ...@@ -48,7 +49,7 @@ def get_parent_xblock(xblock):
Returns the xblock that is the parent of the specified xblock, or None if it has no parent. Returns the xblock that is the parent of the specified xblock, or None if it has no parent.
""" """
locator = xblock.location locator = xblock.location
parent_locations = modulestore().get_parent_locations(locator, None) parent_locations = modulestore().get_parent_locations(locator,)
if len(parent_locations) == 0: if len(parent_locations) == 0:
return None return None
...@@ -79,7 +80,7 @@ def _xblock_has_studio_page(xblock): ...@@ -79,7 +80,7 @@ def _xblock_has_studio_page(xblock):
return False return False
def xblock_studio_url(xblock, course=None): def xblock_studio_url(xblock):
""" """
Returns the Studio editing URL for the specified xblock. Returns the Studio editing URL for the specified xblock.
""" """
...@@ -92,13 +93,9 @@ def xblock_studio_url(xblock, course=None): ...@@ -92,13 +93,9 @@ def xblock_studio_url(xblock, course=None):
else: else:
parent_category = None parent_category = None
if category == 'course': if category == 'course':
prefix = 'course' return reverse_course_url('course_handler', xblock.location.course_key)
elif category == 'vertical' and parent_category == 'sequential': 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: else:
prefix = 'container' return reverse_usage_url('container_handler', xblock.location)
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)
...@@ -12,8 +12,8 @@ from edxmako.shortcuts import render_to_string ...@@ -12,8 +12,8 @@ from edxmako.shortcuts import render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore.django import modulestore, loc_mapper, ModuleI18nService from xmodule.modulestore.django import modulestore, ModuleI18nService
from xmodule.modulestore.locator import Locator from xmodule.modulestore.keys import UsageKey
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xblock.runtime import KvsFieldData from xblock.runtime import KvsFieldData
from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.django.request import webob_to_django_response, django_to_webob_request
...@@ -21,7 +21,6 @@ from xblock.exceptions import NoSuchHandlerError ...@@ -21,7 +21,6 @@ from xblock.exceptions import NoSuchHandlerError
from xblock.fragment import Fragment from xblock.fragment import Fragment
from lms.lib.xblock.field_data import LmsFieldData 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 cms.lib.xblock.runtime import local_resource_url
from util.sandboxing import can_execute_unsafe_code from util.sandboxing import can_execute_unsafe_code
...@@ -29,7 +28,6 @@ from util.sandboxing import can_execute_unsafe_code ...@@ -29,7 +28,6 @@ from util.sandboxing import can_execute_unsafe_code
import static_replace import static_replace
from .session_kv_store import SessionKeyValueStore from .session_kv_store import SessionKeyValueStore
from .helpers import render_from_lms from .helpers import render_from_lms
from ..utils import get_course_for_item
from contentstore.views.access import get_user_role from contentstore.views.access import get_user_role
...@@ -39,19 +37,17 @@ log = logging.getLogger(__name__) ...@@ -39,19 +37,17 @@ log = logging.getLogger(__name__)
@login_required @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 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 handler: The handler to execute
suffix: The remainder of the url to be passed to the handler 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 usage_key = UsageKey.from_string(usage_key_string)
# future it will be the string representation of a Locator.
location = unquote_slashes(usage_id)
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(usage_key)
instance = _load_preview_module(request, descriptor) instance = _load_preview_module(request, descriptor)
# Let the module handle the AJAX # Let the module handle the AJAX
req = django_to_webob_request(request) req = django_to_webob_request(request)
...@@ -88,7 +84,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -88,7 +84,7 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False): def handler_url(self, block, handler_name, suffix='', query='', thirdparty=False):
return reverse('preview_handler', kwargs={ 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, 'handler': handler_name,
'suffix': suffix, 'suffix': suffix,
}) + '?' + query }) + '?' + query
...@@ -106,16 +102,12 @@ def _preview_module_system(request, descriptor): ...@@ -106,16 +102,12 @@ def _preview_module_system(request, descriptor):
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
if isinstance(descriptor.location, Locator): course_id = descriptor.location.course_key
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
display_name_only = (descriptor.category == 'static_tab') display_name_only = (descriptor.category == 'static_tab')
wrappers = [ wrappers = [
# This wrapper wraps the module in the template specified above # 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 # This wrapper replaces urls in the output that start with /static
# with the correct course-specific url for the static content # with the correct course-specific url for the static content
...@@ -141,9 +133,7 @@ def _preview_module_system(request, descriptor): ...@@ -141,9 +133,7 @@ def _preview_module_system(request, descriptor):
# Set up functions to modify the fragment produced by student_view # Set up functions to modify the fragment produced by student_view
wrappers=wrappers, wrappers=wrappers,
error_descriptor_class=ErrorDescriptor, error_descriptor_class=ErrorDescriptor,
# get_user_role accepts a location or a CourseLocator. get_user_role=lambda: get_user_role(request.user, course_id),
# If descriptor.location is a CourseLocator, course_id is unused.
get_user_role=lambda: get_user_role(request.user, descriptor.location, course_id),
descriptor_runtime=descriptor.runtime, descriptor_runtime=descriptor.runtime,
services={ services={
"i18n": ModuleI18nService(), "i18n": ModuleI18nService(),
...@@ -173,11 +163,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -173,11 +163,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
""" """
# Only add the Studio wrapper when on the container page. The unit page will remain as is for now. # Only add the Studio wrapper when on the container page. The unit page will remain as is for now.
if context.get('container_view', None) and view == 'student_view': if context.get('container_view', None) and view == 'student_view':
locator = loc_mapper().translate_location(xblock.course_id, xblock.location, published=False)
template_context = { template_context = {
'xblock_context': context, 'xblock_context': context,
'xblock': xblock, 'xblock': xblock,
'locator': locator,
'content': frag.content, 'content': frag.content,
} }
if xblock.category == 'vertical': if xblock.category == 'vertical':
......
...@@ -22,7 +22,7 @@ def signup(request): ...@@ -22,7 +22,7 @@ def signup(request):
""" """
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect('/course') return redirect('/course/')
if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'): if settings.FEATURES.get('AUTH_USE_CERTIFICATES_IMMEDIATE_SIGNUP'):
# Redirect to course to login to process their certificate if SSL is enabled # Redirect to course to login to process their certificate if SSL is enabled
# and registration is disabled. # and registration is disabled.
...@@ -43,7 +43,7 @@ def login_page(request): ...@@ -43,7 +43,7 @@ def login_page(request):
# SSL login doesn't require a login view, so redirect # SSL login doesn't require a login view, so redirect
# to course now that the user is authenticated via # to course now that the user is authenticated via
# the decorator. # the decorator.
return redirect('/course') return redirect('/course/')
if settings.FEATURES.get('AUTH_USE_CAS'): if settings.FEATURES.get('AUTH_USE_CAS'):
# If CAS is enabled, redirect auth handling to there # If CAS is enabled, redirect auth handling to there
return redirect(reverse('cas-login')) return redirect(reverse('cas-login'))
...@@ -61,6 +61,6 @@ def login_page(request): ...@@ -61,6 +61,6 @@ def login_page(request):
def howitworks(request): def howitworks(request):
"Proxy view" "Proxy view"
if request.user.is_authenticated(): if request.user.is_authenticated():
return redirect('/course') return redirect('/course/')
else: else:
return render_to_response('howitworks.html', {}) return render_to_response('howitworks.html', {})
...@@ -12,11 +12,10 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -12,11 +12,10 @@ from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore 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.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'] __all__ = ['tabs_handler']
...@@ -24,7 +23,7 @@ __all__ = ['tabs_handler'] ...@@ -24,7 +23,7 @@ __all__ = ['tabs_handler']
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT")) @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. The restful handler for static tabs.
...@@ -38,13 +37,11 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -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. 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). 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) course_key = CourseKey.from_string(course_key_string)
if not has_course_access(request.user, locator): if not has_course_access(request.user, course_key):
raise PermissionDenied() raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(locator) course_item = modulestore().get_course(course_key)
store = get_modulestore(old_location)
course_item = store.get_item(old_location)
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
if request.method == 'GET': if request.method == 'GET':
...@@ -68,16 +65,13 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -68,16 +65,13 @@ def tabs_handler(request, tag=None, package_id=None, branch=None, version_guid=N
): ):
if isinstance(tab, StaticTab): if isinstance(tab, StaticTab):
# static tab needs its locator information to render itself as an xmodule # 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) static_tab_loc = course_key.make_usage_key('static_tab', tab.url_slug)
tab.locator = loc_mapper().translate_location( tab.locator = static_tab_loc
course_item.location.course_id, static_tab_loc, False, True
)
tabs_to_render.append(tab) tabs_to_render.append(tab)
return render_to_response('edit-tabs.html', { return render_to_response('edit-tabs.html', {
'context_course': course_item, 'context_course': course_item,
'tabs_to_render': tabs_to_render, 'tabs_to_render': tabs_to_render,
'course_locator': locator,
'lms_link': get_lms_link_for_item(course_item.location), 'lms_link': get_lms_link_for_item(course_item.location),
}) })
else: else:
...@@ -164,11 +158,11 @@ def get_tab_by_tab_id_locator(tab_list, tab_id_locator): ...@@ -164,11 +158,11 @@ def get_tab_by_tab_id_locator(tab_list, tab_id_locator):
return tab 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. 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) item = modulestore('direct').get_item(tab_location)
static_tab = StaticTab( static_tab = StaticTab(
name=item.display_name, name=item.display_name,
......
...@@ -3,63 +3,46 @@ Tests access.py ...@@ -3,63 +3,46 @@ Tests access.py
""" """
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User 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.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import AdminFactory from student.tests.factories import AdminFactory
from student.auth import add_users from student.auth import add_users
from contentstore.views.access import get_user_role from contentstore.views.access import get_user_role
from xmodule.modulestore.locations import SlashSeparatedCourseKey
class RolesTest(TestCase): class RolesTest(TestCase):
""" """
Tests for user roles. Tests for lti user role serialization.
""" """
def setUp(self): def setUp(self):
""" Test case setup """ """ Test case setup """
self.global_admin = AdminFactory() self.global_admin = AdminFactory()
self.instructor = User.objects.create_user('testinstructor', 'testinstructor+courses@edx.org', 'foo') 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.staff = User.objects.create_user('teststaff', 'teststaff+courses@edx.org', 'foo')
self.location = Location('i4x', 'mitX', '101', 'course', 'test') self.course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
self.locator = CourseLocator(url='edx://mitX.101.test')
def test_get_user_role_instructor(self): def test_get_user_role_instructor(self):
""" """
Verifies if user is instructor. 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( self.assertEqual(
'instructor', 'instructor',
get_user_role(self.instructor, self.location, self.location.course_id) get_user_role(self.instructor, self.course_key)
) )
add_users(self.global_admin, CourseStaffRole(self.course_key), self.staff)
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)
self.assertEqual( self.assertEqual(
'instructor', 'instructor',
get_user_role(self.instructor, self.locator) get_user_role(self.instructor, self.course_key)
) )
def test_get_user_role_staff(self): def test_get_user_role_staff(self):
""" """
Verifies if user is staff. 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( self.assertEqual(
'staff', 'staff',
get_user_role(self.staff, self.locator) get_user_role(self.staff, self.course_key)
) )
...@@ -12,13 +12,13 @@ from pytz import UTC ...@@ -12,13 +12,13 @@ from pytz import UTC
import json import json
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.views import assets from contentstore.views import assets
from contentstore.utils import reverse_course_url
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import Location
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import loc_mapper
from django.test.utils import override_settings from django.test.utils import override_settings
from xmodule.modulestore.locations import SlashSeparatedCourseKey, AssetLocation
class AssetsTestCase(CourseTestCase): class AssetsTestCase(CourseTestCase):
...@@ -27,8 +27,7 @@ class AssetsTestCase(CourseTestCase): ...@@ -27,8 +27,7 @@ class AssetsTestCase(CourseTestCase):
""" """
def setUp(self): def setUp(self):
super(AssetsTestCase, self).setUp() super(AssetsTestCase, self).setUp()
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.url = reverse_course_url('assets_handler', self.course.id)
self.url = location.url_reverse('assets/', '')
def upload_asset(self, name="asset-1"): def upload_asset(self, name="asset-1"):
f = BytesIO(name) f = BytesIO(name)
...@@ -42,7 +41,9 @@ class BasicAssetsTestCase(AssetsTestCase): ...@@ -42,7 +41,9 @@ class BasicAssetsTestCase(AssetsTestCase):
self.assertEquals(resp.status_code, 200) self.assertEquals(resp.status_code, 200)
def test_static_url_generation(self): 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) path = StaticContent.get_static_path_from_location(location)
self.assertEquals(path, '/static/my_file_name.jpg') self.assertEquals(path, '/static/my_file_name.jpg')
...@@ -56,13 +57,12 @@ class BasicAssetsTestCase(AssetsTestCase): ...@@ -56,13 +57,12 @@ class BasicAssetsTestCase(AssetsTestCase):
verbose=True verbose=True
) )
course = course_items[0] course = course_items[0]
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True) url = reverse_course_url('assets_handler', course.id)
url = location.url_reverse('assets/', '')
# Test valid contentType for pdf asset (textbook.pdf) # Test valid contentType for pdf asset (textbook.pdf)
resp = self.client.get(url, HTTP_ACCEPT='application/json') resp = self.client.get(url, HTTP_ACCEPT='application/json')
self.assertContains(resp, "/c4x/edX/toy/asset/textbook.pdf") 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) content = contentstore().find(asset_location)
# Check after import textbook.pdf has valid contentType ('application/pdf') # Check after import textbook.pdf has valid contentType ('application/pdf')
...@@ -122,8 +122,7 @@ class UploadTestCase(AssetsTestCase): ...@@ -122,8 +122,7 @@ class UploadTestCase(AssetsTestCase):
""" """
def setUp(self): def setUp(self):
super(UploadTestCase, self).setUp() super(UploadTestCase, self).setUp()
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.url = reverse_course_url('assets_handler', self.course.id)
self.url = location.url_reverse('assets/', '')
def test_happy_path(self): def test_happy_path(self):
resp = self.upload_asset() resp = self.upload_asset()
...@@ -143,18 +142,19 @@ class AssetToJsonTestCase(AssetsTestCase): ...@@ -143,18 +142,19 @@ class AssetToJsonTestCase(AssetsTestCase):
def test_basic(self): def test_basic(self):
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name.jpg']) course_key = SlashSeparatedCourseKey('org', 'class', 'run')
thumbnail_location = Location(['i4x', 'foo', 'bar', 'asset', 'my_file_name_thumb.jpg']) 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) output = assets._get_asset_json("my_file", upload_date, location, thumbnail_location, True)
self.assertEquals(output["display_name"], "my_file") self.assertEquals(output["display_name"], "my_file")
self.assertEquals(output["date_added"], "Jun 01, 2013 at 10:30 UTC") 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["url"], "/c4x/org/class/asset/my_file_name.jpg")
self.assertEquals(output["external_url"], "lms_base_url/i4x/foo/bar/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["portable_url"], "/static/my_file_name.jpg")
self.assertEquals(output["thumbnail"], "/i4x/foo/bar/asset/my_file_name_thumb.jpg") self.assertEquals(output["thumbnail"], "/c4x/org/class/thumbnail/my_file_name_thumb.jpg")
self.assertEquals(output["id"], output["url"]) self.assertEquals(output["id"], unicode(location))
self.assertEquals(output['locked'], True) self.assertEquals(output['locked'], True)
output = assets._get_asset_json("name", upload_date, location, None, False) output = assets._get_asset_json("name", upload_date, location, None, False)
...@@ -176,12 +176,11 @@ class LockAssetTestCase(AssetsTestCase): ...@@ -176,12 +176,11 @@ class LockAssetTestCase(AssetsTestCase):
content = contentstore().find(asset_location) content = contentstore().find(asset_location)
self.assertEqual(content.locked, locked) self.assertEqual(content.locked, locked)
def post_asset_update(lock): def post_asset_update(lock, course):
""" Helper method for posting asset update. """ """ Helper method for posting asset update. """
upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC) upload_date = datetime(2013, 6, 1, 10, 30, tzinfo=UTC)
asset_location = Location(['c4x', 'edX', 'toy', 'asset', 'sample_static.txt']) asset_location = course.id.make_asset_key('asset', 'sample_static.txt')
location = loc_mapper().translate_location(course.location.course_id, course.location, False, True) url = reverse_course_url('assets_handler', course.id, kwargs={'asset_key_string': unicode(asset_location)})
url = location.url_reverse('assets/', '')
resp = self.client.post( resp = self.client.post(
url, url,
...@@ -204,11 +203,11 @@ class LockAssetTestCase(AssetsTestCase): ...@@ -204,11 +203,11 @@ class LockAssetTestCase(AssetsTestCase):
verify_asset_locked_state(False) verify_asset_locked_state(False)
# Lock the asset # Lock the asset
resp_asset = post_asset_update(True) resp_asset = post_asset_update(True, course)
self.assertTrue(resp_asset['locked']) self.assertTrue(resp_asset['locked'])
verify_asset_locked_state(True) verify_asset_locked_state(True)
# Unlock the asset # Unlock the asset
resp_asset = post_asset_update(False) resp_asset = post_asset_update(False, course)
self.assertFalse(resp_asset['locked']) self.assertFalse(resp_asset['locked'])
verify_asset_locked_state(False) verify_asset_locked_state(False)
""" Unit tests for checklist methods in views.py. """ """ 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 contentstore.views.checklist import expand_checklist_action_url
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.django import loc_mapper
import json import json
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
...@@ -14,8 +13,11 @@ class ChecklistTestCase(CourseTestCase): ...@@ -14,8 +13,11 @@ class ChecklistTestCase(CourseTestCase):
""" Creates the test course. """ """ Creates the test course. """
super(ChecklistTestCase, self).setUp() super(ChecklistTestCase, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Checklists Course') 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.get_url()
self.checklists_url = self.location.url_reverse('checklists/', '')
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): def get_persisted_checklists(self):
""" Returns the checklists as persisted in the modulestore. """ """ Returns the checklists as persisted in the modulestore. """
...@@ -41,7 +43,7 @@ class ChecklistTestCase(CourseTestCase): ...@@ -41,7 +43,7 @@ class ChecklistTestCase(CourseTestCase):
response = self.client.get(self.checklists_url) response = self.client.get(self.checklists_url)
self.assertContains(response, "Getting Started With Studio") self.assertContains(response, "Getting Started With Studio")
# Verify expansion of action URL happened. # 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. # Verify persisted checklist does NOT have expanded URL.
checklist_0 = self.get_persisted_checklists()[0] checklist_0 = self.get_persisted_checklists()[0]
self.assertEqual('ManageUsers', get_action_url(checklist_0, 0)) self.assertEqual('ManageUsers', get_action_url(checklist_0, 0))
...@@ -77,7 +79,7 @@ class ChecklistTestCase(CourseTestCase): ...@@ -77,7 +79,7 @@ class ChecklistTestCase(CourseTestCase):
def test_update_checklists_index_ignored_on_get(self): def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """ """ 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) returned_checklists = json.loads(self.client.get(update_url).content)
for pay, resp in zip(self.get_persisted_checklists(), returned_checklists): for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
...@@ -90,14 +92,14 @@ class ChecklistTestCase(CourseTestCase): ...@@ -90,14 +92,14 @@ class ChecklistTestCase(CourseTestCase):
def test_update_checklists_index_out_of_range(self): def test_update_checklists_index_out_of_range(self):
""" Checklist index out of range, will error on post. """ """ 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) response = self.client.post(update_url)
self.assertContains(response, 'Could not save checklist', status_code=400) self.assertContains(response, 'Could not save checklist', status_code=400)
def test_update_checklists_index(self): def test_update_checklists_index(self):
""" Check that an update of a particular checklist works. """ """ 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] payload = self.course.checklists[1]
self.assertFalse(get_first_item(payload).get('is_checked')) self.assertFalse(get_first_item(payload).get('is_checked'))
...@@ -114,7 +116,7 @@ class ChecklistTestCase(CourseTestCase): ...@@ -114,7 +116,7 @@ class ChecklistTestCase(CourseTestCase):
def test_update_checklists_delete_unsupported(self): def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """ """ 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) response = self.client.delete(update_url)
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
...@@ -135,8 +137,8 @@ class ChecklistTestCase(CourseTestCase): ...@@ -135,8 +137,8 @@ class ChecklistTestCase(CourseTestCase):
# Verify no side effect in the original list. # Verify no side effect in the original list.
self.assertEqual(get_action_url(checklist, index), stored) 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[0], 0, 'ManageUsers', '/course_team/slashes:mitX+333+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[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/') test_expansion(self.course.checklists[2], 0, 'http://help.edge.edx.org/', 'http://help.edge.edx.org/')
......
...@@ -28,19 +28,14 @@ class ContainerViewTestCase(CourseTestCase): ...@@ -28,19 +28,14 @@ class ContainerViewTestCase(CourseTestCase):
category="video", display_name="My Video") category="video", display_name="My Video")
def test_container_html(self): def test_container_html(self):
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block"
self._test_html_content( self._test_html_content(
self.child_vertical, self.child_vertical,
branch_name=branch_name, expected_location_in_section_tag=self.child_vertical.location,
expected_section_tag=(
'<section class="wrapper-xblock level-page is-hidden" '
'data-locator="{branch_name}/Child_Vertical">'.format(branch_name=branch_name)
),
expected_breadcrumbs=( expected_breadcrumbs=(
r'<a href="/unit/{branch_name}/Unit"\s*' r'<a href="/unit/{unit_location}"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*' r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>' r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'
).format(branch_name=branch_name) ).format(unit_location=(unicode(self.vertical.location).replace("+", "\\+")))
) )
def test_container_on_container_html(self): def test_container_on_container_html(self):
...@@ -57,58 +52,54 @@ class ContainerViewTestCase(CourseTestCase): ...@@ -57,58 +52,54 @@ class ContainerViewTestCase(CourseTestCase):
category="html", display_name="Child HTML" category="html", display_name="Child HTML"
) )
draft_xblock_with_child = modulestore('draft').convert_to_draft(published_xblock_with_child.location) draft_xblock_with_child = modulestore('draft').convert_to_draft(published_xblock_with_child.location)
branch_name = "MITx.999.Robot_Super_Course/branch/draft/block" expected_breadcrumbs = (
r'<a href="/unit/{unit_location}"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="/container/{child_vertical_location}"\s*'
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
).format(
unit_location=unicode(self.vertical.location).replace("+", "\\+"),
child_vertical_location=unicode(self.child_vertical.location).replace("+", "\\+"),
)
self._test_html_content( self._test_html_content(
published_xblock_with_child, published_xblock_with_child,
branch_name=branch_name, expected_location_in_section_tag=published_xblock_with_child.location,
expected_section_tag=( expected_breadcrumbs=expected_breadcrumbs
'<section class="wrapper-xblock level-page is-hidden" '
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
),
expected_breadcrumbs=(
r'<a href="/unit/{branch_name}/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="/container/{branch_name}/Child_Vertical"\s*'
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
).format(branch_name=branch_name)
) )
self._test_html_content( self._test_html_content(
draft_xblock_with_child, draft_xblock_with_child,
branch_name=branch_name, expected_location_in_section_tag=draft_xblock_with_child.location,
expected_section_tag=( expected_breadcrumbs=expected_breadcrumbs
'<section class="wrapper-xblock level-page is-hidden" '
'data-locator="{branch_name}/Wrapper">'.format(branch_name=branch_name)
),
expected_breadcrumbs=(
r'<a href="/unit/{branch_name}/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="/container/{branch_name}/Child_Vertical"\s*'
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
).format(branch_name=branch_name)
) )
def _test_html_content(self, xblock, branch_name, expected_section_tag, expected_breadcrumbs): def _test_html_content(self, xblock, expected_location_in_section_tag, expected_breadcrumbs):
""" """
Get the HTML for a container page and verify the section tag is correct Get the HTML for a container page and verify the section tag is correct
and the breadcrumbs trail is correct. and the breadcrumbs trail is correct.
""" """
url = xblock_studio_url(xblock, self.course)
publish_state = compute_publish_state(xblock) publish_state = compute_publish_state(xblock)
url = xblock_studio_url(xblock)
resp = self.client.get_html(url) resp = self.client.get_html(url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
html = resp.content html = resp.content
expected_section_tag = \
'<section class="wrapper-xblock level-page is-hidden" ' \
'data-locator="{child_location}" ' \
'data-course-key="{course_key}">'.format(
child_location=unicode(expected_location_in_section_tag),
course_key=unicode(expected_location_in_section_tag.course_key)
)
self.assertIn(expected_section_tag, html) self.assertIn(expected_section_tag, html)
# Verify the navigation link at the top of the page is correct. # Verify the navigation link at the top of the page is correct.
self.assertRegexpMatches(html, expected_breadcrumbs) self.assertRegexpMatches(html, expected_breadcrumbs)
# Verify the link that allows users to change publish status. # Verify the link that allows users to change publish status.
expected_message = None
if publish_state == PublishState.public: 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_location}">Unit</a> as a draft.'
else: else:
expected_message = 'your changes will be published with unit <a href="/unit/{branch_name}/Unit">Unit</a>.' expected_message = 'your changes will be published with unit <a href="/unit/{unit_location}">Unit</a>.'
expected_unit_link = expected_message.format( expected_unit_link = expected_message.format(
branch_name=branch_name unit_location=unicode(self.vertical.location)
) )
self.assertIn(expected_unit_link, html) self.assertIn(expected_unit_link, html)
...@@ -5,7 +5,7 @@ import json ...@@ -5,7 +5,7 @@ import json
import lxml import lxml
from contentstore.tests.utils import CourseTestCase 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.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import parsers from xmodule.modulestore import parsers
...@@ -30,7 +30,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -30,7 +30,7 @@ class TestCourseIndex(CourseTestCase):
""" """
Test getting the list of courses and then pulling up their outlines 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') index_response = authed_client.get(index_url, {}, HTTP_ACCEPT='text/html')
parsed_html = lxml.html.fromstring(index_response.content) parsed_html = lxml.html.fromstring(index_response.content)
course_link_eles = parsed_html.find_class('course-link') course_link_eles = parsed_html.find_class('course-link')
...@@ -38,7 +38,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -38,7 +38,7 @@ class TestCourseIndex(CourseTestCase):
for link in course_link_eles: for link in course_link_eles:
self.assertRegexpMatches( self.assertRegexpMatches(
link.get("href"), link.get("href"),
r'course/{0}+/branch/{0}+/block/{0}+'.format(parsers.ALLOWED_ID_CHARS) 'course/slashes:{0}'.format(parsers.ALLOWED_ID_CHARS)
) )
# now test that url # now test that url
outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html') outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html')
...@@ -59,7 +59,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -59,7 +59,7 @@ class TestCourseIndex(CourseTestCase):
""" """
Test the error conditions for the access 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 # register a non-staff member and try to delete the course branch
non_staff_client, _ = self.create_non_staff_authed_user_client() non_staff_client, _ = self.create_non_staff_authed_user_client()
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json') response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
...@@ -67,12 +67,11 @@ class TestCourseIndex(CourseTestCase): ...@@ -67,12 +67,11 @@ class TestCourseIndex(CourseTestCase):
def test_course_staff_access(self): 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() course_staff_client, course_staff = self.create_non_staff_authed_user_client()
for course in [self.course, self.odd_course]: for course in [self.course, self.odd_course]:
new_location = loc_mapper().translate_location(course.location.course_id, course.location, False, True) permission_url = reverse_course_url('course_team_handler', course.id, kwargs={'email': course_staff.email})
permission_url = new_location.url_reverse("course_team/", course_staff.email)
self.client.post( self.client.post(
permission_url, permission_url,
...@@ -85,7 +84,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -85,7 +84,7 @@ class TestCourseIndex(CourseTestCase):
self.check_index_and_outline(course_staff_client) self.check_index_and_outline(course_staff_client)
def test_json_responses(self): 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") 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") 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') subsection = ItemFactory.create(parent_location=lesson.location, category='vertical', display_name='Subsection 1')
...@@ -96,17 +95,17 @@ class TestCourseIndex(CourseTestCase): ...@@ -96,17 +95,17 @@ class TestCourseIndex(CourseTestCase):
# First spot check some values in the root response # First spot check some values in the root response
self.assertEqual(json_response['category'], 'course') 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.assertEqual(json_response['display_name'], 'Robot Super Course')
self.assertTrue(json_response['is_container']) self.assertTrue(json_response['is_container'])
self.assertFalse(json_response['is_draft']) self.assertFalse(json_response['is_draft'])
# Now verify that the first child # Now verify the first child
children = json_response['children'] children = json_response['children']
self.assertTrue(len(children) > 0) self.assertTrue(len(children) > 0)
first_child_response = children[0] first_child_response = children[0]
self.assertEqual(first_child_response['category'], 'chapter') 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.assertEqual(first_child_response['display_name'], 'Week 1')
self.assertTrue(first_child_response['is_container']) self.assertTrue(first_child_response['is_container'])
self.assertFalse(first_child_response['is_draft']) self.assertFalse(first_child_response['is_draft'])
......
...@@ -12,42 +12,34 @@ class HelpersTestCase(CourseTestCase): ...@@ -12,42 +12,34 @@ class HelpersTestCase(CourseTestCase):
Unit tests for helpers.py. Unit tests for helpers.py.
""" """
def test_xblock_studio_url(self): def test_xblock_studio_url(self):
course = self.course
# Verify course URL # Verify course URL
self.assertEqual(xblock_studio_url(course), self.assertEqual(xblock_studio_url(self.course),
u'/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course') u'/course/slashes:MITx+999+Robot_Super_Course')
# Verify chapter URL # Verify chapter URL
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
display_name="Week 1") display_name="Week 1")
self.assertIsNone(xblock_studio_url(chapter)) self.assertIsNone(xblock_studio_url(chapter))
self.assertIsNone(xblock_studio_url(chapter, course))
# Verify lesson URL # Verify lesson URL
sequential = ItemFactory.create(parent_location=chapter.location, category='sequential', sequential = ItemFactory.create(parent_location=chapter.location, category='sequential',
display_name="Lesson 1") display_name="Lesson 1")
self.assertIsNone(xblock_studio_url(sequential)) self.assertIsNone(xblock_studio_url(sequential))
self.assertIsNone(xblock_studio_url(sequential, course))
# Verify vertical URL # Verify vertical URL
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical', vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
display_name='Unit') display_name='Unit')
self.assertEqual(xblock_studio_url(vertical), self.assertEqual(xblock_studio_url(vertical),
u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit') u'/unit/location:MITx+999+Robot_Super_Course+vertical+Unit')
self.assertEqual(xblock_studio_url(vertical, course),
u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit')
# Verify child vertical URL # Verify child vertical URL
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical', child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
display_name='Child Vertical') display_name='Child Vertical')
self.assertEqual(xblock_studio_url(child_vertical), self.assertEqual(xblock_studio_url(child_vertical),
u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical') u'/container/location:MITx+999+Robot_Super_Course+vertical+Child_Vertical')
self.assertEqual(xblock_studio_url(child_vertical, course),
u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical')
# Verify video URL # Verify video URL
video = ItemFactory.create(parent_location=child_vertical.location, category="video", video = ItemFactory.create(parent_location=child_vertical.location, category="video",
display_name="My Video") display_name="My Video")
self.assertIsNone(xblock_studio_url(video)) self.assertIsNone(xblock_studio_url(video))
self.assertIsNone(xblock_studio_url(video, course))
...@@ -15,7 +15,7 @@ from pymongo import MongoClient ...@@ -15,7 +15,7 @@ from pymongo import MongoClient
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from xmodule.modulestore.django import loc_mapper from contentstore.utils import reverse_course_url
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.tests.factories import ItemFactory from xmodule.modulestore.tests.factories import ItemFactory
...@@ -33,10 +33,7 @@ class ImportTestCase(CourseTestCase): ...@@ -33,10 +33,7 @@ class ImportTestCase(CourseTestCase):
""" """
def setUp(self): def setUp(self):
super(ImportTestCase, self).setUp() super(ImportTestCase, self).setUp()
self.new_location = loc_mapper().translate_location( self.url = reverse_course_url('import_handler', self.course.id)
self.course.location.course_id, self.course.location, False, True
)
self.url = self.new_location.url_reverse('import/', '')
self.content_dir = path(tempfile.mkdtemp()) self.content_dir = path(tempfile.mkdtemp())
def touch(name): def touch(name):
...@@ -88,9 +85,10 @@ class ImportTestCase(CourseTestCase): ...@@ -88,9 +85,10 @@ class ImportTestCase(CourseTestCase):
# Check that `import_status` returns the appropriate stage (i.e., the # Check that `import_status` returns the appropriate stage (i.e., the
# stage at which import failed). # stage at which import failed).
resp_status = self.client.get( resp_status = self.client.get(
self.new_location.url_reverse( reverse_course_url(
'import_status', 'import_status_handler',
os.path.split(self.bad_tar)[1] self.course.id,
kwargs={'filename': os.path.split(self.bad_tar)[1]}
) )
) )
...@@ -192,9 +190,10 @@ class ImportTestCase(CourseTestCase): ...@@ -192,9 +190,10 @@ class ImportTestCase(CourseTestCase):
# either 3, indicating all previous steps are completed, or 0, # either 3, indicating all previous steps are completed, or 0,
# indicating no upload in progress) # indicating no upload in progress)
resp_status = self.client.get( resp_status = self.client.get(
self.new_location.url_reverse( reverse_course_url(
'import_status', 'import_status_handler',
os.path.split(self.good_tar)[1] self.course.id,
kwargs={'filename': os.path.split(self.good_tar)[1]}
) )
) )
import_status = json.loads(resp_status.content)["ImportStatus"] import_status = json.loads(resp_status.content)["ImportStatus"]
...@@ -211,8 +210,7 @@ class ExportTestCase(CourseTestCase): ...@@ -211,8 +210,7 @@ class ExportTestCase(CourseTestCase):
Sets up the test course. Sets up the test course.
""" """
super(ExportTestCase, self).setUp() super(ExportTestCase, self).setUp()
location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.url = reverse_course_url('export_handler', self.course.id)
self.url = location.url_reverse('export/', '')
def test_export_html(self): def test_export_html(self):
""" """
...@@ -253,7 +251,7 @@ class ExportTestCase(CourseTestCase): ...@@ -253,7 +251,7 @@ class ExportTestCase(CourseTestCase):
Export failure. Export failure.
""" """
ItemFactory.create(parent_location=self.course.location, category='aawefawef') 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): def test_export_failure_subsection_level(self):
""" """
...@@ -264,7 +262,8 @@ class ExportTestCase(CourseTestCase): ...@@ -264,7 +262,8 @@ class ExportTestCase(CourseTestCase):
parent_location=vertical.location, parent_location=vertical.location,
category='aawefawef' 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): def _verify_export_failure(self, expectedText):
""" Export failure helper method. """ """ Export failure helper method. """
......
...@@ -7,22 +7,24 @@ from django.test.client import RequestFactory ...@@ -7,22 +7,24 @@ from django.test.client import RequestFactory
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.django import loc_mapper
from contentstore.views.preview import get_preview_fragment from contentstore.views.preview import get_preview_fragment
class GetPreviewHtmlTestCase(TestCase): 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 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() course = CourseFactory.create()
html = ItemFactory.create( html = ItemFactory.create(
...@@ -31,25 +33,16 @@ class GetPreviewHtmlTestCase(TestCase): ...@@ -31,25 +33,16 @@ class GetPreviewHtmlTestCase(TestCase):
data={'data': "<html>foobar</html>"} 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 = RequestFactory().get('/dummy-url')
request.user = UserFactory() request.user = UserFactory()
request.session = {} request.session = {}
# Must call get_preview_fragment directly, as going through xblock RESTful API will attempt # Call get_preview_fragment directly.
# to use item.location as a Location.
html = get_preview_fragment(request, html, {}).content 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( self.assertRegexpMatches(
html, 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.assertRegexpMatches(html, '<html>foobar</html>')
self.assertNotRegexpMatches(html, 'i4x')
...@@ -5,8 +5,9 @@ from contentstore.views import tabs ...@@ -5,8 +5,9 @@ from contentstore.views import tabs
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from courseware.courses import get_course_by_id
from xmodule.tabs import CourseTabList, WikiTab from xmodule.tabs import CourseTabList, WikiTab
from contentstore.utils import reverse_course_url
from xmodule.modulestore.django import modulestore
class TabsPageTests(CourseTestCase): class TabsPageTests(CourseTestCase):
...@@ -19,11 +20,11 @@ class TabsPageTests(CourseTestCase): ...@@ -19,11 +20,11 @@ class TabsPageTests(CourseTestCase):
super(TabsPageTests, self).setUp() super(TabsPageTests, self).setUp()
# Set the URL for tests # 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 # add a static tab to the course, for code coverage
ItemFactory.create( ItemFactory.create(
parent_location=self.course_location, parent_location=self.course.location,
category="static_tab", category="static_tab",
display_name="Static_1" display_name="Static_1"
) )
...@@ -204,5 +205,5 @@ class PrimitiveTabEdit(TestCase): ...@@ -204,5 +205,5 @@ class PrimitiveTabEdit(TestCase):
"""Test course saving.""" """Test course saving."""
course = CourseFactory.create(org='edX', course='999') course = CourseFactory.create(org='edX', course='999')
tabs.primitive_insert(course, 3, 'notes', 'aname') 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'}) self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'})
import json import json
from unittest import TestCase from unittest import TestCase
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.utils import get_modulestore from contentstore.utils import reverse_course_url
from contentstore.views.course import ( from contentstore.views.course import (
validate_textbooks_json, validate_textbook_json, TextbookValidationError) validate_textbooks_json, validate_textbook_json, TextbookValidationError)
...@@ -12,7 +12,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -12,7 +12,7 @@ class TextbookIndexTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set the URL for tests" "Set the URL for tests"
super(TextbookIndexTestCase, self).setUp() 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): def test_view_index(self):
"Basic check that the textbook index page responds correctly" "Basic check that the textbook index page responds correctly"
...@@ -110,7 +110,8 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -110,7 +110,8 @@ class TextbookCreateTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set up a url and some textbook content for tests" "Set up a url and some textbook content for tests"
super(TextbookCreateTestCase, self).setUp() 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 = { self.textbook = {
"tab_title": "Economics", "tab_title": "Economics",
"chapters": { "chapters": {
...@@ -177,7 +178,8 @@ class TextbookDetailTestCase(CourseTestCase): ...@@ -177,7 +178,8 @@ class TextbookDetailTestCase(CourseTestCase):
"url": "/a/b/c/ch1.pdf", "url": "/a/b/c/ch1.pdf",
} }
} }
self.url1 = self.course_locator.url_reverse("textbooks", "1") self.url1 = self.get_details_url("1")
self.textbook2 = { self.textbook2 = {
"tab_title": "Algebra", "tab_title": "Algebra",
"id": 2, "id": 2,
...@@ -186,12 +188,22 @@ class TextbookDetailTestCase(CourseTestCase): ...@@ -186,12 +188,22 @@ class TextbookDetailTestCase(CourseTestCase):
"url": "/a/b/ch11.pdf", "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] self.course.pdf_textbooks = [self.textbook1, self.textbook2]
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
self.save_course() 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): def test_get_1(self):
"Get the first textbook" "Get the first textbook"
...@@ -233,7 +245,7 @@ class TextbookDetailTestCase(CourseTestCase): ...@@ -233,7 +245,7 @@ class TextbookDetailTestCase(CourseTestCase):
"url": "supercool.pdf", "url": "supercool.pdf",
"id": "1supercool", "id": "1supercool",
} }
url = self.course_locator.url_reverse("textbooks", "1supercool") url = self.get_details_url("1supercool")
resp = self.client.post( resp = self.client.post(
url, url,
data=json.dumps(textbook), data=json.dumps(textbook),
......
...@@ -17,14 +17,16 @@ from django.contrib.auth.decorators import login_required ...@@ -17,14 +17,16 @@ from django.contrib.auth.decorators import login_required
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from opaque_keys import InvalidKeyError
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.exceptions import NotFoundError 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.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 util.json_request import JsonResponse
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.video_module.transcripts_utils import ( from xmodule.video_module.transcripts_utils import (
generate_subs_from_source, generate_subs_from_source,
...@@ -32,7 +34,6 @@ from xmodule.video_module.transcripts_utils import ( ...@@ -32,7 +34,6 @@ from xmodule.video_module.transcripts_utils import (
download_youtube_subs, get_transcripts_from_youtube, download_youtube_subs, get_transcripts_from_youtube,
copy_or_rename_transcript, copy_or_rename_transcript,
manage_video_subtitles_save, manage_video_subtitles_save,
TranscriptsGenerationException,
GetTranscriptsFromYouTubeException, GetTranscriptsFromYouTubeException,
TranscriptsRequestValidationException TranscriptsRequestValidationException
) )
...@@ -84,7 +85,7 @@ def upload_transcripts(request): ...@@ -84,7 +85,7 @@ def upload_transcripts(request):
try: try:
item = _get_item(request, request.POST) item = _get_item(request, request.POST)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError): except (InvalidKeyError, ItemNotFoundError):
return error_response(response, "Can't find item by locator.") return error_response(response, "Can't find item by locator.")
if 'transcript-file' not in request.FILES: if 'transcript-file' not in request.FILES:
...@@ -149,7 +150,7 @@ def download_transcripts(request): ...@@ -149,7 +150,7 @@ def download_transcripts(request):
try: try:
item = _get_item(request, request.GET) item = _get_item(request, request.GET)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError): except (InvalidKeyError, ItemNotFoundError):
log.debug("Can't find item by locator.") log.debug("Can't find item by locator.")
raise Http404 raise Http404
...@@ -163,9 +164,7 @@ def download_transcripts(request): ...@@ -163,9 +164,7 @@ def download_transcripts(request):
raise Http404 raise Http404
filename = 'subs_{0}.srt.sjson'.format(subs_id) filename = 'subs_{0}.srt.sjson'.format(subs_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(item.location.course_key, filename)
item.location.org, item.location.course, filename
)
try: try:
sjson_transcripts = contentstore().find(content_location) sjson_transcripts = contentstore().find(content_location)
log.debug("Downloading subs for %s id", subs_id) log.debug("Downloading subs for %s id", subs_id)
...@@ -227,9 +226,7 @@ def check_transcripts(request): ...@@ -227,9 +226,7 @@ def check_transcripts(request):
transcripts_presence['status'] = 'Success' transcripts_presence['status'] = 'Success'
filename = 'subs_{0}.srt.sjson'.format(item.sub) filename = 'subs_{0}.srt.sjson'.format(item.sub)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(item.location.course_key, filename)
item.location.org, item.location.course, filename
)
try: try:
local_transcripts = contentstore().find(content_location).data local_transcripts = contentstore().find(content_location).data
transcripts_presence['current_item_subs'] = item.sub transcripts_presence['current_item_subs'] = item.sub
...@@ -243,9 +240,7 @@ def check_transcripts(request): ...@@ -243,9 +240,7 @@ def check_transcripts(request):
# youtube local # youtube local
filename = 'subs_{0}.srt.sjson'.format(youtube_id) filename = 'subs_{0}.srt.sjson'.format(youtube_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(item.location.course_key, filename)
item.location.org, item.location.course, filename
)
try: try:
local_transcripts = contentstore().find(content_location).data local_transcripts = contentstore().find(content_location).data
transcripts_presence['youtube_local'] = True transcripts_presence['youtube_local'] = True
...@@ -276,9 +271,7 @@ def check_transcripts(request): ...@@ -276,9 +271,7 @@ def check_transcripts(request):
html5_subs = [] html5_subs = []
for html5_id in videos['html5']: for html5_id in videos['html5']:
filename = 'subs_{0}.srt.sjson'.format(html5_id) filename = 'subs_{0}.srt.sjson'.format(html5_id)
content_location = StaticContent.compute_location( content_location = StaticContent.compute_location(item.location.course_key, filename)
item.location.org, item.location.course, filename
)
try: try:
html5_subs.append(contentstore().find(content_location).data) html5_subs.append(contentstore().find(content_location).data)
transcripts_presence['html5_local'].append(html5_id) transcripts_presence['html5_local'].append(html5_id)
...@@ -438,7 +431,7 @@ def _validate_transcripts_data(request): ...@@ -438,7 +431,7 @@ def _validate_transcripts_data(request):
try: try:
item = _get_item(request, data) item = _get_item(request, data)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError): except (InvalidKeyError, ItemNotFoundError):
raise TranscriptsRequestValidationException(_("Can't find item by locator.")) raise TranscriptsRequestValidationException(_("Can't find item by locator."))
if item.category != 'video': if item.category != 'video':
...@@ -503,7 +496,7 @@ def save_transcripts(request): ...@@ -503,7 +496,7 @@ def save_transcripts(request):
try: try:
item = _get_item(request, data) item = _get_item(request, data)
except (ItemNotFoundError, InvalidLocationError, InsufficientSpecificationError): except (InvalidKeyError, ItemNotFoundError):
return error_response(response, "Can't find item by locator.") return error_response(response, "Can't find item by locator.")
metadata = data.get('metadata') metadata = data.get('metadata')
...@@ -538,14 +531,13 @@ def _get_item(request, data): ...@@ -538,14 +531,13 @@ def _get_item(request, data):
Returns the item. Returns the item.
""" """
locator = BlockUsageLocator(data.get('locator')) usage_key = UsageKey.from_string(data.get('locator'))
old_location = loc_mapper().translate_locator_to_location(locator)
# This is placed before has_course_access() to validate the location, # This is placed before has_course_access() to validate the location,
# because has_course_access() raises InvalidLocationError if location is invalid. # because has_course_access() raises r if location is invalid.
item = modulestore().get_item(old_location) 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() raise PermissionDenied()
return item return item
...@@ -7,15 +7,15 @@ from django.views.decorators.http import require_POST ...@@ -7,15 +7,15 @@ from django.views.decorators.http import require_POST
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response 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 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 course_creators.views import user_requested_access
from .access import has_course_access from .access import has_course_access
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.locator import BlockUsageLocator
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from student import auth from student import auth
...@@ -37,7 +37,7 @@ def request_course_creator(request): ...@@ -37,7 +37,7 @@ def request_course_creator(request):
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE")) @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. The restful handler for course team users.
...@@ -49,51 +49,49 @@ def course_team_handler(request, tag=None, package_id=None, branch=None, version ...@@ -49,51 +49,49 @@ def course_team_handler(request, tag=None, package_id=None, branch=None, version
DELETE: DELETE:
json: remove a particular course team member from the course team (email is required). 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) course_key = CourseKey.from_string(course_key_string) if course_key_string else None
if not has_course_access(request.user, location): if not has_course_access(request.user, course_key):
raise PermissionDenied() raise PermissionDenied()
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): 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 elif request.method == 'GET': # assume html
return _manage_users(request, location) return _manage_users(request, course_key)
else: else:
return HttpResponseNotFound() 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 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 # 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() raise PermissionDenied()
course_module = modulestore().get_item(old_location) course_module = modulestore().get_course(course_key)
instructors = CourseInstructorRole(locator).users_with_role() 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. # 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', { return render_to_response('manage_users.html', {
'context_course': course_module, 'context_course': course_module,
'staff': staff, 'staff': staff,
'instructors': instructors, '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 @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 Handle the add, remove, promote, demote requests ensuring the requester has authority
""" """
# check that logged in user has permissions to this item # 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 # instructors have full permissions
pass 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 # staff can only affect themselves
pass pass
else: else:
...@@ -102,6 +100,7 @@ def _course_team_user(request, locator, email): ...@@ -102,6 +100,7 @@ def _course_team_user(request, locator, email):
} }
return JsonResponse(msg, 400) return JsonResponse(msg, 400)
try: try:
user = User.objects.get(email=email) user = User.objects.get(email=email)
except Exception: except Exception:
...@@ -119,7 +118,7 @@ def _course_team_user(request, locator, email): ...@@ -119,7 +118,7 @@ def _course_team_user(request, locator, email):
"role": None, "role": None,
} }
# what's the highest role that this user has? (How should this report global staff?) # 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): if role.has_user(user):
msg["role"] = role.ROLE msg["role"] = role.ROLE
break break
...@@ -134,11 +133,11 @@ def _course_team_user(request, locator, email): ...@@ -134,11 +133,11 @@ def _course_team_user(request, locator, email):
if request.method == "DELETE": if request.method == "DELETE":
try: try:
try_remove_instructor(request, locator, user) try_remove_instructor(request, course_key, user)
except CannotOrphanCourse as oops: except CannotOrphanCourse as oops:
return JsonResponse(oops.msg, 400) return JsonResponse(oops.msg, 400)
auth.remove_users(request.user, CourseStaffRole(locator), user) auth.remove_users(request.user, CourseStaffRole(course_key), user)
return JsonResponse() return JsonResponse()
# all other operations require the requesting user to specify a role # all other operations require the requesting user to specify a role
...@@ -146,27 +145,26 @@ def _course_team_user(request, locator, email): ...@@ -146,27 +145,26 @@ def _course_team_user(request, locator, email):
if role is None: if role is None:
return JsonResponse({"error": _("`role` is required")}, 400) return JsonResponse({"error": _("`role` is required")}, 400)
old_location = loc_mapper().translate_locator_to_location(locator)
if role == "instructor": 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 = { msg = {
"error": _("Only instructors may create other instructors") "error": _("Only instructors may create other instructors")
} }
return JsonResponse(msg, 400) 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. # 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": elif role == "staff":
# add to staff regardless (can't do after removing from instructors as will no longer # add to staff regardless (can't do after removing from instructors as will no longer
# be allowed) # be allowed)
auth.add_users(request.user, CourseStaffRole(locator), user) auth.add_users(request.user, CourseStaffRole(course_key), user)
try: try:
try_remove_instructor(request, locator, user) try_remove_instructor(request, course_key, user)
except CannotOrphanCourse as oops: except CannotOrphanCourse as oops:
return JsonResponse(oops.msg, 400) return JsonResponse(oops.msg, 400)
# auto-enroll the course creator in the course so that "View Live" will work. # 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() return JsonResponse()
...@@ -180,13 +178,14 @@ class CannotOrphanCourse(Exception): ...@@ -180,13 +178,14 @@ class CannotOrphanCourse(Exception):
Exception.__init__(self) 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 # remove all roles in this course from this user: but fail if the user
# is the last instructor in the course team # is the last instructor in the course team
instructors = CourseInstructorRole(locator) instructors = CourseInstructorRole(course_key)
if instructors.has_user(user): if instructors.has_user(user):
if instructors.users_with_role().count() == 1: 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) raise CannotOrphanCourse(msg)
else: else:
auth.remove_users(request.user, instructors, user) auth.remove_users(request.user, instructors, user)
...@@ -9,8 +9,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -9,8 +9,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.utils import get_modulestore, course_image_url from contentstore.utils import get_modulestore, course_image_url
from models.settings import course_grading from models.settings import course_grading
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import modulestore
class CourseDetails(object): class CourseDetails(object):
def __init__(self, org, course_id, run): def __init__(self, org, course_id, run):
...@@ -31,61 +30,60 @@ class CourseDetails(object): ...@@ -31,61 +30,60 @@ class CourseDetails(object):
self.course_image_asset_path = "" # URL of the course image self.course_image_asset_path = "" # URL of the course image
@classmethod @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. 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 = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location) course_details = cls(course_key.org, course_key.course, course_key.run)
course = cls(course_old_location.org, course_old_location.course, course_old_location.name)
course_details.start_date = descriptor.start
course.start_date = descriptor.start course_details.end_date = descriptor.end
course.end_date = descriptor.end course_details.enrollment_start = descriptor.enrollment_start
course.enrollment_start = descriptor.enrollment_start course_details.enrollment_end = descriptor.enrollment_end
course.enrollment_end = descriptor.enrollment_end course_details.course_image_name = descriptor.course_image
course.course_image_name = descriptor.course_image course_details.course_image_asset_path = course_image_url(descriptor)
course.course_image_asset_path = course_image_url(descriptor)
temploc = course_key.make_usage_key('about', 'syllabus')
temploc = course_old_location.replace(category='about', name='syllabus')
try: try:
course.syllabus = get_modulestore(temploc).get_item(temploc).data course_details.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = course_old_location.replace(category='about', name='short_description') temploc = course_key.make_usage_key('about', 'short_description')
try: try:
course.short_description = get_modulestore(temploc).get_item(temploc).data course_details.short_description = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc.replace(name='overview') temploc = course_key.make_usage_key('about', 'overview')
try: try:
course.overview = get_modulestore(temploc).get_item(temploc).data course_details.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc.replace(name='effort') temploc = course_key.make_usage_key('about', 'effort')
try: try:
course.effort = get_modulestore(temploc).get_item(temploc).data course_details.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc.replace(name='video') temploc = course_key.make_usage_key('about', 'video')
try: try:
raw_video = get_modulestore(temploc).get_item(temploc).data 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: except ItemNotFoundError:
pass pass
return course return course_details
@classmethod @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 Update the about item with the new data blob. If data is None, then
delete the about item. 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) store = get_modulestore(temploc)
if data is None: if data is None:
store.delete_item(temploc) store.delete_item(temploc)
...@@ -98,12 +96,12 @@ class CourseDetails(object): ...@@ -98,12 +96,12 @@ class CourseDetails(object):
store.update_item(about_item, user.id) store.update_item(about_item, user.id)
@classmethod @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 Decode the json into CourseDetails and save any changed attrs to the db
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_locator) module_store = modulestore('direct')
descriptor = get_modulestore(course_old_location).get_item(course_old_location) descriptor = module_store.get_course(course_key)
dirty = False dirty = False
...@@ -153,19 +151,19 @@ class CourseDetails(object): ...@@ -153,19 +151,19 @@ class CourseDetails(object):
dirty = True dirty = True
if dirty: 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 # 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. # 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']: 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']) 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 # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly # it persisted correctly
return CourseDetails.fetch(course_locator) return CourseDetails.fetch(course_key)
@staticmethod @staticmethod
def parse_video_tag(raw_video): def parse_video_tag(raw_video):
......
from datetime import timedelta from datetime import timedelta
from contentstore.utils import get_modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import loc_mapper
from xblock.fields import Scope from xblock.fields import Scope
...@@ -18,25 +17,21 @@ class CourseGradingModel(object): ...@@ -18,25 +17,21 @@ class CourseGradingModel(object):
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
@classmethod @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. 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 = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
model = cls(descriptor) model = cls(descriptor)
return model return model
@staticmethod @staticmethod
def fetch_grader(course_location, index): def fetch_grader(course_key, index):
""" """
Fetch the course's nth grader Fetch the course's nth grader
Returns an empty dict if there's no such grader. Returns an empty dict if there's no such grader.
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_location) descriptor = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
index = int(index) index = int(index)
if len(descriptor.raw_grader) > index: if len(descriptor.raw_grader) > index:
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -52,33 +47,31 @@ class CourseGradingModel(object): ...@@ -52,33 +47,31 @@ class CourseGradingModel(object):
} }
@staticmethod @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. 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. 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 = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']] graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
descriptor.raw_grader = graders_parsed descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs'] 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 @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 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) 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 = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
# parse removes the id; so, grab it before parse # parse removes the id; so, grab it before parse
index = int(grader.get('id', len(descriptor.raw_grader))) index = int(grader.get('id', len(descriptor.raw_grader)))
...@@ -89,33 +82,31 @@ class CourseGradingModel(object): ...@@ -89,33 +82,31 @@ class CourseGradingModel(object):
else: else:
descriptor.raw_grader.append(grader) 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]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
@staticmethod @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 Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
db fetch). db fetch).
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_location) descriptor = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
descriptor.grade_cutoffs = cutoffs descriptor.grade_cutoffs = cutoffs
get_modulestore(course_old_location).update_item(descriptor, user.id) modulestore('direct').update_item(descriptor, user.id)
return cutoffs return cutoffs
@staticmethod @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 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 grace_period entry in an enclosing dict. It is also safe to call this method with a value of
None for graceperiodjson. None for graceperiodjson.
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_location) descriptor = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
# Before a graceperiod has ever been created, it will be None (once it has been # Before a graceperiod has ever been created, it will be None (once it has been
# created, it cannot be set back to None). # created, it cannot be set back to None).
...@@ -126,15 +117,14 @@ class CourseGradingModel(object): ...@@ -126,15 +117,14 @@ class CourseGradingModel(object):
grace_timedelta = timedelta(**graceperiodjson) grace_timedelta = timedelta(**graceperiodjson)
descriptor.graceperiod = grace_timedelta descriptor.graceperiod = grace_timedelta
get_modulestore(course_old_location).update_item(descriptor, user.id) modulestore('direct').update_item(descriptor, user.id)
@staticmethod @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. Delete the grader of the given type from the given course.
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_location) descriptor = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
index = int(index) index = int(index)
if index < len(descriptor.raw_grader): if index < len(descriptor.raw_grader):
...@@ -142,24 +132,22 @@ class CourseGradingModel(object): ...@@ -142,24 +132,22 @@ class CourseGradingModel(object):
# force propagation to definition # force propagation to definition
descriptor.raw_grader = descriptor.raw_grader descriptor.raw_grader = descriptor.raw_grader
get_modulestore(course_old_location).update_item(descriptor, user.id) modulestore('direct').update_item(descriptor, user.id)
@staticmethod @staticmethod
def delete_grace_period(course_location, user): def delete_grace_period(course_key, user):
""" """
Delete the course's grace period. Delete the course's grace period.
""" """
course_old_location = loc_mapper().translate_locator_to_location(course_location) descriptor = modulestore('direct').get_course(course_key)
descriptor = get_modulestore(course_old_location).get_item(course_old_location)
del descriptor.graceperiod del descriptor.graceperiod
get_modulestore(course_old_location).update_item(descriptor, user.id) modulestore('direct').update_item(descriptor, user.id)
@staticmethod @staticmethod
def get_section_grader_type(location): def get_section_grader_type(location):
old_location = loc_mapper().translate_locator_to_location(location) descriptor = modulestore('direct').get_item(location)
descriptor = get_modulestore(old_location).get_item(old_location)
return { return {
"graderType": descriptor.format if descriptor.format is not None else 'notgraded', "graderType": descriptor.format if descriptor.format is not None else 'notgraded',
"location": unicode(location), "location": unicode(location),
...@@ -174,7 +162,7 @@ class CourseGradingModel(object): ...@@ -174,7 +162,7 @@ class CourseGradingModel(object):
del descriptor.format del descriptor.format
del descriptor.graded del descriptor.graded
get_modulestore(descriptor.location).update_item(descriptor, user.id) modulestore('direct').update_item(descriptor, user.id)
return {'graderType': grader_type} return {'graderType': grader_type}
@staticmethod @staticmethod
......
...@@ -4,8 +4,6 @@ XBlock runtime implementations for edX Studio ...@@ -4,8 +4,6 @@ XBlock runtime implementations for edX Studio
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from lms.lib.xblock.runtime import quote_slashes
def handler_url(block, handler_name, suffix='', query='', thirdparty=False): def handler_url(block, handler_name, suffix='', query='', thirdparty=False):
""" """
...@@ -16,7 +14,7 @@ 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") raise NotImplementedError("edX Studio doesn't support third-party xblock handler urls")
url = reverse('component_handler', kwargs={ 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, 'handler': handler_name,
'suffix': suffix, 'suffix': suffix,
}).rstrip('/') }).rstrip('/')
......
...@@ -13,7 +13,6 @@ class TestHandlerUrl(TestCase): ...@@ -13,7 +13,6 @@ class TestHandlerUrl(TestCase):
def setUp(self): def setUp(self):
self.block = Mock() self.block = Mock()
self.course_id = "org/course/run"
def test_trailing_charecters(self): def test_trailing_charecters(self):
self.assertFalse(handler_url(self.block, 'handler').endswith('?')) self.assertFalse(handler_url(self.block, 'handler').endswith('?'))
......
...@@ -20,11 +20,12 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1", ...@@ -20,11 +20,12 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
createItem: (parent, payload, callback=->) -> createItem: (parent, payload, callback=->) ->
payload.parent_locator = parent payload.parent_locator = parent
$.postJSON( $.postJSON(
@model.urlRoot @model.urlRoot + '/'
payload payload
(data) => (data) =>
@model.set(id: data.locator) @model.set(id: data.locator)
@$el.data('locator', data.locator) @$el.data('locator', data.locator)
@$el.data('courseKey', data.courseKey)
@render() @render()
).success(callback) ).success(callback)
......
...@@ -32,7 +32,7 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"], ...@@ -32,7 +32,7 @@ require(["domReady", "jquery", "underscore", "js/utils/cancel_on_escape"],
'run': run 'run': run
}); });
$.postJSON('/course', { $.postJSON('/course/', {
'org': org, 'org': org,
'number': number, 'number': number,
'display_name': display_name, 'display_name': display_name,
......
...@@ -7,7 +7,7 @@ define(['js/utils/module'], ...@@ -7,7 +7,7 @@ define(['js/utils/module'],
}); });
describe('getUpdateUrl ', function () { describe('getUpdateUrl ', function () {
it('can take no arguments', function () { it('can take no arguments', function () {
expect(ModuleUtils.getUpdateUrl()).toBe('/xblock'); expect(ModuleUtils.getUpdateUrl()).toBe('/xblock/');
}); });
it('appends a locator', function () { it('appends a locator', function () {
expect(ModuleUtils.getUpdateUrl("locator")).toBe('/xblock/locator'); expect(ModuleUtils.getUpdateUrl("locator")).toBe('/xblock/locator');
......
...@@ -3,7 +3,7 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec_helpers/creat ...@@ -3,7 +3,7 @@ define(["coffee/src/views/unit", "js/models/module_info", "js/spec_helpers/creat
function (UnitEditView, ModuleModel, create_sinon, NotificationView) { function (UnitEditView, ModuleModel, create_sinon, NotificationView) {
var verifyJSON = function (requests, json) { var verifyJSON = function (requests, json) {
var request = requests[requests.length - 1]; var request = requests[requests.length - 1];
expect(request.url).toEqual("/xblock"); expect(request.url).toEqual("/xblock/");
expect(request.method).toEqual("POST"); expect(request.method).toEqual("POST");
// There was a problem with order of returned parameters in strings. // There was a problem with order of returned parameters in strings.
// Changed to compare objects instead strings. // Changed to compare objects instead strings.
......
...@@ -12,7 +12,7 @@ define([], function () { ...@@ -12,7 +12,7 @@ define([], function () {
var getUpdateUrl = function (locator) { var getUpdateUrl = function (locator) {
if (locator === undefined) { if (locator === undefined) {
return urlRoot; return urlRoot + "/";
} }
else { else {
return urlRoot + "/" + locator; return urlRoot + "/" + locator;
......
...@@ -14,7 +14,8 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V ...@@ -14,7 +14,8 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
initialize : function() { initialize : function() {
var self = this, var self = this,
counter = 0, 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.template = this.loadTemplate('metadata-editor');
this.$el.html(this.template({numEntries: this.collection.length})); this.$el.html(this.template({numEntries: this.collection.length}));
...@@ -23,6 +24,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V ...@@ -23,6 +24,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
function (model) { function (model) {
var data = { var data = {
el: self.$el.find('.metadata_entry')[counter++], el: self.$el.find('.metadata_entry')[counter++],
courseKey: courseKey,
locator: locator, locator: locator,
model: model model: model
}, },
...@@ -528,7 +530,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V ...@@ -528,7 +530,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, FileUpload, UploadDialog, V
upload: function (event) { upload: function (event) {
var self = this, var self = this,
target = $(event.currentTarget), target = $(event.currentTarget),
url = /assets/ + this.options.locator, url = '/assets/' + this.options.courseKey + '/',
model = new FileUpload({ model = new FileUpload({
title: gettext('Upload File'), title: gettext('Upload File'),
}), }),
......
...@@ -190,6 +190,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", ...@@ -190,6 +190,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
xblockElement = xblockWrapperElement.find('.xblock'); xblockElement = xblockWrapperElement.find('.xblock');
xblockInfo = new XBlockInfo({ xblockInfo = new XBlockInfo({
id: xblockWrapperElement.data('locator'), id: xblockWrapperElement.data('locator'),
courseKey: xblockWrapperElement.data('course-key'),
category: xblockElement.data('block-type') category: xblockElement.data('block-type')
}); });
} }
......
...@@ -56,7 +56,7 @@ main_xblock_info = { ...@@ -56,7 +56,7 @@ main_xblock_info = {
<small class="navigation navigation-parents"> <small class="navigation navigation-parents">
% for ancestor in ancestor_xblocks: % for ancestor in ancestor_xblocks:
<% <%
ancestor_url = xblock_studio_url(ancestor, context_course) ancestor_url = xblock_studio_url(ancestor)
%> %>
% if ancestor_url: % if ancestor_url:
<a href="${ancestor_url}" <a href="${ancestor_url}"
...@@ -80,7 +80,7 @@ main_xblock_info = { ...@@ -80,7 +80,7 @@ main_xblock_info = {
<section class="content-area"> <section class="content-area">
<article class="content-primary window"> <article class="content-primary window">
<section class="wrapper-xblock level-page is-hidden" data-locator="${xblock_locator}"> <section class="wrapper-xblock level-page is-hidden" data-locator="${xblock_locator}" data-course-key="${xblock_locator.course_key}">
</section> </section>
<div class="no-container-content is-hidden"> <div class="no-container-content is-hidden">
<p>${_("This page has no content yet.")}</p> <p>${_("This page has no content yet.")}</p>
......
...@@ -4,7 +4,7 @@ from contentstore.views.helpers import xblock_studio_url ...@@ -4,7 +4,7 @@ from contentstore.views.helpers import xblock_studio_url
%> %>
<%namespace name='static' file='static_content.html'/> <%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"> <header class="xblock-header">
<div class="header-details"> <div class="header-details">
${xblock.display_name_with_default} ${xblock.display_name_with_default}
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"],
function (TabsModel, TabsEditView) { function (TabsModel, TabsEditView) {
var model = new TabsModel({ var model = new TabsModel({
id: "${course_locator}", id: "${context_course.location}",
explicit_url: "${course_locator.url_reverse('tabs')}" explicit_url: "${reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': context_course.id})}"
}); });
new TabsEditView({ new TabsEditView({
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="main-wrapper"> <div class="main-wrapper">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="main-column"> <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"> <div class="subsection-name-input">
<label>${_("Display Name:")}</label> <label>${_("Display Name:")}</label>
<input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/> <input type="text" value="${subsection.display_name_with_default | h}" class="subsection-display-name-input" data-metadata-name="display_name"/>
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</div> </div>
<div class="sidebar"> <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> <h4 class="header">${_("Subsection Settings")}</h4>
<div class="window-contents"> <div class="window-contents">
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import json import json
%> %>
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
% else: % else:
<ul class="list-actions"> <ul class="list-actions">
<li class="item-action"> <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> <i class="icon-download"></i>
<span class="copy">${_("Export to Git")}</span> <span class="copy">${_("Export to Git")}</span>
</a> </a>
......
<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 @@ ...@@ -2,7 +2,6 @@
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%! from student.roles import CourseInstructorRole %> <%! from student.roles import CourseInstructorRole %>
<%! from xmodule.modulestore.django import loc_mapper %>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%block name="title">${_("Course Team Settings")}</%block> <%block name="title">${_("Course Team Settings")}</%block>
<%block name="bodyclass">is-signedin course users view-team</%block> <%block name="bodyclass">is-signedin course users view-team</%block>
...@@ -60,12 +59,16 @@ ...@@ -60,12 +59,16 @@
%endif %endif
<ol class="user-list"> <ol class="user-list">
<% new_location = loc_mapper().translate_location(context_course.location.course_id, context_course.location, False, True) %>
% for user in staff: % 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: % if is_instuctor:
<span class="wrapper-ui-badge"> <span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-admin is-hanging"> <span class="flag flag-role flag-role-admin is-hanging">
...@@ -120,7 +123,7 @@ ...@@ -120,7 +123,7 @@
% endfor % endfor
</ol> </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: % if user_is_instuctor and len(staff) == 1:
<div class="notice notice-incontext notice-create has-actions"> <div class="notice notice-incontext notice-create has-actions">
<div class="msg"> <div class="msg">
...@@ -163,9 +166,7 @@ require(["jquery", "underscore", "gettext", "js/views/feedback_prompt"], ...@@ -163,9 +166,7 @@ require(["jquery", "underscore", "gettext", "js/views/feedback_prompt"],
function($, _, gettext, PromptView) { function($, _, gettext, PromptView) {
var staffEmails = ${json.dumps([user.email for user in staff])}; var staffEmails = ${json.dumps([user.email for user in staff])};
var tplUserURL = "${loc_mapper().\ var tplUserURL = "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}"
translate_location(context_course.location.course_id, context_course.location, False, True).\
url_reverse('course_team/', "@@EMAIL@@")}";
var unknownErrorMessage = gettext("Unknown"); var unknownErrorMessage = gettext("Unknown");
......
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
import logging import logging
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from contentstore.utils import reverse_usage_url
from xmodule.modulestore.django import loc_mapper
%> %>
<%block name="title">${_("Course Outline")}</%block> <%block name="title">${_("Course Outline")}</%block>
<%block name="bodyclass">is-signedin course view-outline</%block> <%block name="bodyclass">is-signedin course view-outline</%block>
...@@ -56,7 +55,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -56,7 +55,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<h3 class="section-name"> <h3 class="section-name">
<form class="section-name-form"> <form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" /> <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')}" /> data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="${_('Cancel')}" /> <input type="button" class="new-section-name-cancel" value="${_('Cancel')}" />
</form> </form>
...@@ -76,7 +75,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -76,7 +75,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<span class="section-name-span">${_('Add a new section name')}</span> <span class="section-name-span">${_('Add a new section name')}</span>
<form class="section-name-form"> <form class="section-name-form">
<input type="text" value="${_('New Section Name')}" class="new-section-name" /> <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')}" /> data-category="${new_section_category}" value="${_('Save')}" />
<input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3> <input type="button" class="new-section-name-cancel" value="$(_('Cancel')}" /></h3>
</form> </form>
...@@ -148,16 +147,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -148,16 +147,12 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="wrapper-dnd"> <div class="wrapper-dnd">
<% <%
course_locator = loc_mapper().translate_location( course_locator = context_course.location
context_course.location.course_id, context_course.location, False, True
)
%> %>
<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: % for section in sections:
<% <%
section_locator = loc_mapper().translate_location( section_locator = section.location
context_course.location.course_id, section.location, False, True
)
%> %>
<section class="courseware-section is-collapsible is-draggable" data-parent="${course_locator}" <section class="courseware-section is-collapsible is-draggable" data-parent="${course_locator}"
data-locator="${section_locator}"> data-locator="${section_locator}">
...@@ -207,9 +202,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -207,9 +202,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<ol class="sortable-subsection-list"> <ol class="sortable-subsection-list">
% for subsection in section.get_children(): % for subsection in section.get_children():
<% <%
subsection_locator = loc_mapper().translate_location( subsection_locator = subsection.location
context_course.location.course_id, subsection.location, False, True
)
%> %>
<li class="courseware-subsection collapsed id-holder is-draggable is-collapsible " <li class="courseware-subsection collapsed id-holder is-draggable is-collapsible "
data-parent="${section_locator}" data-locator="${subsection_locator}"> data-parent="${section_locator}" data-locator="${subsection_locator}">
...@@ -219,7 +212,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -219,7 +212,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
<div class="section-item"> <div class="section-item">
<div class="details"> <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="#" 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> <span class="subsection-name"><span class="subsection-name-value">${subsection.display_name_with_default}</span></span>
</a> </a>
</div> </div>
......
...@@ -118,7 +118,7 @@ require(["jquery", "jquery.cookie"], function($) { ...@@ -118,7 +118,7 @@ require(["jquery", "jquery.cookie"], function($) {
data: submit_data, data: submit_data,
headers: {'X-CSRFToken': $.cookie('csrftoken')}, headers: {'X-CSRFToken': $.cookie('csrftoken')},
success: function(json) { success: function(json) {
location.href = "/course"; location.href = "/course/";
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
json = $.parseJSON(jqXHR.responseText); json = $.parseJSON(jqXHR.responseText);
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper from contentstore import utils
%> %>
<%block name="header_extras"> <%block name="header_extras">
...@@ -305,9 +305,9 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -305,9 +305,9 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% <%
course_team_url = course_locator.url_reverse('course_team/', '') course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
grading_config_url = course_locator.url_reverse('settings/grading/') grading_config_url = utils.reverse_course_url('grading_handler', context_course.id)
advanced_config_url = course_locator.url_reverse('settings/advanced/') advanced_config_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)
%> %>
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore import utils from contentstore import utils
from xmodule.modulestore.django import loc_mapper
%> %>
<%block name="title">${_("Advanced Settings")}</%block> <%block name="title">${_("Advanced Settings")}</%block>
<%block name="bodyclass">is-signedin course advanced view-settings</%block> <%block name="bodyclass">is-signedin course advanced view-settings</%block>
...@@ -89,11 +88,9 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting ...@@ -89,11 +88,9 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% <%
ctx_loc = context_course.location details_url = utils.reverse_course_url('settings_handler', context_course.id)
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) grading_url = utils.reverse_course_url('grading_handler', context_course.id)
details_url = location.url_reverse('settings/details/') course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
grading_url = location.url_reverse('settings/grading/')
course_team_url = location.url_reverse('course_team/', '')
%> %>
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
<%! <%!
from contentstore import utils from contentstore import utils
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
%> %>
<%block name="header_extras"> <%block name="header_extras">
...@@ -138,9 +137,9 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings ...@@ -138,9 +137,9 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% <%
course_team_url = course_locator.url_reverse('course_team/') detailed_settings_url = utils.reverse_course_url('settings_handler', context_course.id)
advanced_settings_url = course_locator.url_reverse('settings/advanced/') course_team_url = utils.reverse_course_url('course_team_handler', context_course.id)
detailed_settings_url = course_locator.url_reverse('settings/details/') advanced_settings_url = utils.reverse_course_url('advanced_settings_handler', context_course.id)
%> %>
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
% if xblock.location != xblock_context['root_xblock'].location: % if xblock.location != xblock_context['root_xblock'].location:
<section class="wrapper-xblock level-nesting is-collapsible" data-locator="${locator}"> <section class="wrapper-xblock level-nesting is-collapsible" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
% endif % endif
<header class="xblock-header"> <header class="xblock-header">
<div class="header-details"> <div class="header-details">
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
% if xblock.location != xblock_context['root_xblock'].location: % if xblock.location != xblock_context['root_xblock'].location:
<% section_class = "level-nesting" if xblock.has_children else "level-element" %> <% section_class = "level-nesting" if xblock.has_children else "level-element" %>
<section class="wrapper-xblock ${section_class}" data-locator="${locator}" data-display-name="${xblock.display_name_with_default | h}" data-category="${xblock.category | h}"> <section class="wrapper-xblock ${section_class}" data-locator="${xblock.location}" data-display-name="${xblock.display_name_with_default | h}" data-category="${xblock.category | h}" data-course-key="${xblock.location.course_key}">
% endif % endif
<header class="xblock-header"> <header class="xblock-header">
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
from django.core.urlresolvers import reverse from contentstore import utils
from contentstore.views.helpers import EDITING_TEMPLATES from contentstore.views.helpers import EDITING_TEMPLATES
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
%> %>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%namespace name="units" file="widgets/units.html" /> <%namespace name="units" file="widgets/units.html" />
...@@ -46,7 +45,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -46,7 +45,7 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</%block> </%block>
<%block name="content"> <%block name="content">
<div class="main-wrapper edit-state-${unit_state}" data-locator="${unit_locator}"> <div class="main-wrapper edit-state-${unit_state}" data-locator="${unit_locator}" data-course-key="${unit_locator.course_key}">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="alert editing-draft-alert"> <div class="alert editing-draft-alert">
<p class="alert-message"><strong>${_("You are editing a draft.")}</strong> <p class="alert-message"><strong>${_("You are editing a draft.")}</strong>
...@@ -60,8 +59,8 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -60,8 +59,8 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
<article class="unit-body window"> <article class="unit-body window">
<p class="unit-name-input"><label for="unit-display-name-input">${_("Display Name:")}</label><input type="text" value="${unit.display_name_with_default | h}" id="unit-display-name-input" class="unit-display-name-input" /></p> <p class="unit-name-input"><label for="unit-display-name-input">${_("Display Name:")}</label><input type="text" value="${unit.display_name_with_default | h}" id="unit-display-name-input" class="unit-display-name-input" /></p>
<ol class="components"> <ol class="components">
% for locator in locators: % for xblock in xblocks:
<li class="component" data-locator="${locator}"/> <li class="component" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}"/>
% endfor % endfor
<li class="new-component-item adding"> <li class="new-component-item adding">
<div class="new-component"> <div class="new-component">
...@@ -148,11 +147,8 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit" ...@@ -148,11 +147,8 @@ require(["domReady!", "jquery", "js/models/module_info", "coffee/src/views/unit"
</div> </div>
<% <%
ctx_loc = context_course.location index_url = utils.reverse_course_url('course_handler', context_course.id)
index_url = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True).url_reverse('course') subsection_url = utils.reverse_usage_url('subsection_handler', subsection.location)
subsection_url = loc_mapper().translate_location(
ctx_loc.course_id, subsection.location, False, True
).url_reverse('subsection')
%> %>
<div class="sidebar"> <div class="sidebar">
<div class="unit-settings window"> <div class="unit-settings window">
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
<%! <%!
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
%> %>
<div class="wrapper-header wrapper" id="view-top"> <div class="wrapper-header wrapper" id="view-top">
...@@ -14,20 +13,19 @@ ...@@ -14,20 +13,19 @@
% if context_course: % if context_course:
<% <%
ctx_loc = context_course.location course_key = context_course.id
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) index_url = reverse('contentstore.views.course_handler', kwargs={'course_key_string': unicode(course_key)})
index_url = location.url_reverse('course') checklists_url = reverse('contentstore.views.checklists_handler', kwargs={'course_key_string': unicode(course_key)})
checklists_url = location.url_reverse('checklists') course_team_url = reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(course_key)})
course_team_url = location.url_reverse('course_team') assets_url = reverse('contentstore.views.assets_handler', kwargs={'course_key_string': unicode(course_key)})
assets_url = location.url_reverse('assets') textbooks_url = reverse('contentstore.views.textbooks_list_handler', kwargs={'course_key_string': unicode(course_key)})
textbooks_url = location.url_reverse('textbooks') import_url = reverse('contentstore.views.import_handler', kwargs={'course_key_string': unicode(course_key)})
import_url = location.url_reverse('import') course_info_url = reverse('contentstore.views.course_info_handler', kwargs={'course_key_string': unicode(course_key)})
course_info_url = location.url_reverse('course_info') export_url = reverse('contentstore.views.export_handler', kwargs={'course_key_string': unicode(course_key)})
export_url = location.url_reverse('export') settings_url = reverse('contentstore.views.settings_handler', kwargs={'course_key_string': unicode(course_key)})
settings_url = location.url_reverse('settings/details/') grading_url = reverse('contentstore.views.grading_handler', kwargs={'course_key_string': unicode(course_key)})
grading_url = location.url_reverse('settings/grading/') advanced_settings_url = reverse('contentstore.views.advanced_settings_handler', kwargs={'course_key_string': unicode(course_key)})
advanced_settings_url = location.url_reverse('settings/advanced/') tabs_url = reverse('contentstore.views.tabs_handler', kwargs={'course_key_string': unicode(course_key)})
tabs_url = location.url_reverse('tabs')
%> %>
<h2 class="info-course"> <h2 class="info-course">
<span class="sr">${_("Current Course:")}</span> <span class="sr">${_("Current Course:")}</span>
...@@ -106,7 +104,7 @@ ...@@ -106,7 +104,7 @@
</li> </li>
% if settings.FEATURES.get('ENABLE_EXPORT_GIT') and context_course.giturl: % if settings.FEATURES.get('ENABLE_EXPORT_GIT') and context_course.giturl:
<li class="nav-item nav-course-tools-export-git"> <li class="nav-item nav-course-tools-export-git">
<a href="${reverse('export_git', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Export to Git")}</a> <a href="${reverse('export_git', kwargs=dict(course_key_string=unicode(course_key)))}">${_("Export to Git")}</a>
</li> </li>
% endif % endif
</ul> </ul>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment