Commit f6731342 by Calen Pennington

Make Studio load XBlock fragment js and css on the client-side

[LMS-1421][LMS-1517]
parent 49217ebe
#pylint: disable=E1101 #pylint: disable=E1101
import shutil import json
import mock import mock
import shutil
from textwrap import dedent from textwrap import dedent
...@@ -503,7 +504,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -503,7 +504,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
This verifies that a video caption url is as we expect it to be This verifies that a video caption url is as we expect it to be
""" """
resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None)) resp = self._test_preview(Location('i4x', 'edX', 'toy', 'video', 'sample_video', None))
self.assertContains(resp, 'data-caption-asset-path="/c4x/edX/toy/asset/subs_"') self.assertEquals(resp.status_code, 200)
content = json.loads(resp.content)
self.assertIn('data-caption-asset-path="/c4x/edX/toy/asset/subs_"', content['html'])
def _test_preview(self, location): def _test_preview(self, location):
""" Preview test case. """ """ Preview test case. """
...@@ -514,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -514,7 +517,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
locator = loc_mapper().translate_location( locator = loc_mapper().translate_location(
course_items[0].location.course_id, location, True, True course_items[0].location.course_id, location, True, True
) )
resp = self.client.get_html(locator.url_reverse('xblock')) resp = self.client.get_fragment(locator.url_reverse('xblock'))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# TODO: uncomment when preview no longer has locations being returned. # TODO: uncomment when preview no longer has locations being returned.
# _test_no_locations(self, resp) # _test_no_locations(self, resp)
......
...@@ -57,6 +57,13 @@ class AjaxEnabledTestClient(Client): ...@@ -57,6 +57,13 @@ class AjaxEnabledTestClient(Client):
""" """
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra) return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
def get_fragment(self, path, data=None, follow=False, **extra):
"""
Convenience method for client.get which sets the accept type to application/x-fragment+json
"""
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/x-fragment+json", **extra)
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
......
"""Views for items (modules).""" """Views for items (modules)."""
import hashlib
import logging import logging
from uuid import uuid4 from uuid import uuid4
from collections import OrderedDict
from functools import partial from functools import partial
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule_modifiers import wrap_xblock from xmodule_modifiers import wrap_xblock
from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest, HttpResponse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from xblock.fields import Scope from xblock.fields import Scope
from xblock.fragment import Fragment
from xblock.core import XBlock from xblock.core import XBlock
import xmodule.x_module
from xmodule.modulestore.django import modulestore, loc_mapper from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import prefer_xmodules
from util.json_request import expect_json, JsonResponse from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool from util.string_utils import str_to_bool
...@@ -32,8 +36,8 @@ from ..utils import get_modulestore ...@@ -32,8 +36,8 @@ from ..utils import get_modulestore
from .access import has_course_access from .access import has_course_access
from .helpers import _xmodule_recurse from .helpers import _xmodule_recurse
from preview import handler_prefix, get_preview_html from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.runtime import handler_url from cms.lib.xblock.runtime import handler_url
...@@ -50,6 +54,16 @@ CREATE_IF_NOT_FOUND = ['course_info'] ...@@ -50,6 +54,16 @@ CREATE_IF_NOT_FOUND = ['course_info']
xmodule.x_module.descriptor_global_handler_url = handler_url xmodule.x_module.descriptor_global_handler_url = handler_url
def hash_resource(resource):
"""
Hash a :class:`xblock.fragment.FragmentResource
"""
md5 = hashlib.md5()
for data in resource:
md5.update(data)
return md5.hexdigest()
# pylint: disable=unused-argument # pylint: disable=unused-argument
@require_http_methods(("DELETE", "GET", "PUT", "POST")) @require_http_methods(("DELETE", "GET", "PUT", "POST"))
@login_required @login_required
...@@ -95,34 +109,52 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -95,34 +109,52 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
old_location = loc_mapper().translate_locator_to_location(locator) old_location = loc_mapper().translate_locator_to_location(locator)
if request.method == 'GET': if request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): accept_header = request.META.get('HTTP_ACCEPT', 'application/json')
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields: if 'application/x-fragment+json' in accept_header:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
component = modulestore().get_item(old_location) component = modulestore().get_item(old_location)
# Wrap the generated fragment in the xmodule_editor div so that the javascript # Wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly # can bind to it correctly
component.runtime.wrappers.append(partial(wrap_xblock, handler_prefix)) component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime'))
try: try:
content = component.render('studio_view').content editor_fragment = component.render('studio_view')
# catch exceptions indiscriminately, since after this point they escape the # catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable # dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins. # component-goblins.
except Exception as exc: # pylint: disable=W0703 except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render studio_view for %r", component, exc_info=True) log.debug("Unable to render studio_view for %r", component, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)}) editor_fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return render_to_response('component.html', { modulestore().save_xmodule(component)
'preview': get_preview_html(request, component),
'editor': content, preview_fragment = get_preview_fragment(request, component)
'label': component.display_name or component.category,
hashed_resources = OrderedDict()
for resource in editor_fragment.resources + preview_fragment.resources:
hashed_resources[hash_resource(resource)] = resource
return JsonResponse({
'html': render_to_string('component.html', {
'preview': preview_fragment.content,
'editor': editor_fragment.content,
'label': component.display_name or component.scope_ids.block_type,
}),
'resources': hashed_resources.items()
}) })
elif 'application/json' in accept_header:
fields = request.REQUEST.get('fields', '').split(',')
if 'graderType' in fields:
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(locator))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(locator)
return JsonResponse(rsp)
else:
return HttpResponse(status=406)
elif request.method == 'DELETE': elif request.method == 'DELETE':
delete_children = str_to_bool(request.REQUEST.get('recurse', 'False')) delete_children = str_to_bool(request.REQUEST.get('recurse', 'False'))
delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False')) delete_all_versions = str_to_bool(request.REQUEST.get('all_versions', 'False'))
...@@ -288,7 +320,7 @@ def _create_item(request): ...@@ -288,7 +320,7 @@ def _create_item(request):
data = None data = None
template_id = request.json.get('boilerplate') template_id = request.json.get('boilerplate')
if template_id is not None: if template_id is not None:
clz = XBlock.load_class(category, select=prefer_xmodules) clz = parent.runtime.load_block_type(category)
if clz is not None: if clz is not None:
template = clz.get_template(template_id) template = clz.get_template(template_id)
if template is not None: if template is not None:
......
import logging import logging
import hashlib
from functools import partial from functools import partial
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xblock from xmodule_modifiers import replace_static_urls, wrap_xblock
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
...@@ -15,6 +16,7 @@ from xmodule.x_module import ModuleSystem ...@@ -15,6 +16,7 @@ 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
from xblock.exceptions import NoSuchHandlerError from xblock.exceptions import NoSuchHandlerError
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 lms.lib.xblock.runtime import quote_slashes, unquote_slashes
...@@ -143,15 +145,15 @@ def _load_preview_module(request, descriptor): ...@@ -143,15 +145,15 @@ def _load_preview_module(request, descriptor):
return descriptor return descriptor
def get_preview_html(request, descriptor): def get_preview_fragment(request, descriptor):
""" """
Returns the HTML returned by the XModule's student_view, Returns the HTML returned by the XModule's student_view,
specified by the descriptor and idx. specified by the descriptor and idx.
""" """
module = _load_preview_module(request, descriptor) module = _load_preview_module(request, descriptor)
try: try:
content = module.render("student_view").content fragment = module.render("student_view")
except Exception as exc: # pylint: disable=W0703 except Exception as exc: # pylint: disable=W0703
log.debug("Unable to render student_view for %r", module, exc_info=True) log.debug("Unable to render student_view for %r", module, exc_info=True)
content = render_to_string('html_error.html', {'message': str(exc)}) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return content return fragment
define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (ModuleEdit, ModuleModel) -> define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmodule"], ($, ModuleEdit, ModuleModel) ->
describe "ModuleEdit", -> describe "ModuleEdit", ->
beforeEach -> beforeEach ->
...@@ -24,7 +24,7 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo ...@@ -24,7 +24,7 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
</section> </section>
</li> </li>
""" """
spyOn($.fn, 'load').andReturn(@moduleData) spyOn($, 'ajax').andReturn(@moduleData)
@moduleEdit = new ModuleEdit( @moduleEdit = new ModuleEdit(
el: $(".component") el: $(".component")
...@@ -56,14 +56,63 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo ...@@ -56,14 +56,63 @@ define ["coffee/src/views/module_edit", "js/models/module_info", "xmodule"], (Mo
beforeEach -> beforeEach ->
spyOn(@moduleEdit, 'loadDisplay') spyOn(@moduleEdit, 'loadDisplay')
spyOn(@moduleEdit, 'delegateEvents') spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append')
spyOn($, 'getScript')
window.loadedXBlockResources = undefined
@moduleEdit.render() @moduleEdit.render()
$.ajax.mostRecentCall.args[0].success(
html: '<div>Response html</div>'
resources: [
['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}],
['hash2', {kind: 'url', mimetype: 'text/css', data: 'css-url'}],
['hash3', {kind: 'text', mimetype: 'application/javascript', data: 'inline-js'}],
['hash4', {kind: 'url', mimetype: 'application/javascript', data: 'js-url'}],
['hash5', {placement: 'head', mimetype: 'text/html', data: 'head-html'}],
['hash6', {placement: 'not-head', mimetype: 'text/html', data: 'not-head-html'}],
]
)
it "loads the module preview and editor via ajax on the view element", -> it "loads the module preview and editor via ajax on the view element", ->
expect(@moduleEdit.$el.load).toHaveBeenCalledWith("/xblock/#{@moduleEdit.model.id}", jasmine.any(Function)) expect($.ajax).toHaveBeenCalledWith(
@moduleEdit.$el.load.mostRecentCall.args[1]() url: "/xblock/#{@moduleEdit.model.id}"
type: "GET"
headers:
Accept: 'application/x-fragment+json'
success: jasmine.any(Function)
)
expect(@moduleEdit.loadDisplay).toHaveBeenCalled() expect(@moduleEdit.loadDisplay).toHaveBeenCalled()
expect(@moduleEdit.delegateEvents).toHaveBeenCalled() expect(@moduleEdit.delegateEvents).toHaveBeenCalled()
it "loads inline css from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<style type='text/css'>inline-css</style>")
it "loads css urls from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<link rel='stylesheet' href='css-url' type='text/css'>")
it "loads inline js from fragments", ->
expect($('head').append).toHaveBeenCalledWith("<script>inline-js</script>")
it "loads js urls from fragments", ->
expect($.getScript).toHaveBeenCalledWith("js-url")
it "loads head html", ->
expect($('head').append).toHaveBeenCalledWith("head-html")
it "doesn't load body html", ->
expect($.fn.append).not.toHaveBeenCalledWith('not-head-html')
it "doesn't reload resources", ->
count = $('head').append.callCount
$.ajax.mostRecentCall.args[0].success(
html: '<div>Response html 2</div>'
resources: [
['hash1', {kind: 'text', mimetype: 'text/css', data: 'inline-css'}],
]
)
expect($('head').append.callCount).toBe(count)
describe "loadDisplay", -> describe "loadDisplay", ->
beforeEach -> beforeEach ->
spyOn(XBlock, 'initializeBlock') spyOn(XBlock, 'initializeBlock')
......
...@@ -75,9 +75,37 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1", ...@@ -75,9 +75,37 @@ define ["backbone", "jquery", "underscore", "gettext", "xblock/runtime.v1",
render: -> render: ->
if @model.id if @model.id
@$el.load(@model.url(), => $.ajax(
@loadDisplay() url: @model.url()
@delegateEvents() type: 'GET'
headers:
Accept: 'application/x-fragment+json'
success: (data) =>
@$el.html(data.html)
for value in data.resources
do (value) =>
hash = value[0]
if not window.loadedXBlockResources?
window.loadedXBlockResources = []
if hash not in window.loadedXBlockResources
resource = value[1]
switch resource.mimetype
when "text/css"
switch resource.kind
when "text" then $('head').append("<style type='text/css'>#{resource.data}</style>")
when "url" then $('head').append("<link rel='stylesheet' href='#{resource.data}' type='text/css'>")
when "application/javascript"
switch resource.kind
when "text" then $('head').append("<script>#{resource.data}</script>")
when "url" then $.getScript(resource.data)
when "text/html"
switch resource.placement
when "head" then $('head').append(resource.data)
window.loadedXBlockResources.push(hash)
@loadDisplay()
@delegateEvents()
) )
clickSaveButton: (event) => clickSaveButton: (event) =>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.xmodule_VideoModule { .xmodule_VideoModule {
// display mode // display mode
&.xmodule_display { &.xblock-student_view {
// full screen // full screen
.video-controls .add-fullscreen { .video-controls .add-fullscreen {
......
...@@ -183,16 +183,16 @@ ...@@ -183,16 +183,16 @@
border-left: 1px solid $mediumGrey; border-left: 1px solid $mediumGrey;
border-right: 1px solid $mediumGrey; border-right: 1px solid $mediumGrey;
.xmodule_display { .xblock-student_view {
display: none; display: none;
} }
} }
.new .xmodule_display { .new .xblock-student_view {
background: $yellow; background: $yellow;
} }
.xmodule_display { .xblock-student_view {
@include transition(background-color $tmg-s3 linear 0s); @include transition(background-color $tmg-s3 linear 0s);
padding: 20px 20px 22px; padding: 20px 20px 22px;
font-size: 24px; font-size: 24px;
......
...@@ -420,7 +420,7 @@ body.course.unit,.view-unit { ...@@ -420,7 +420,7 @@ body.course.unit,.view-unit {
} }
} }
.xmodule_display { .xblock-student_view {
padding: 2*$baseline $baseline $baseline; padding: 2*$baseline $baseline $baseline;
overflow-x: auto; overflow-x: auto;
......
...@@ -2,9 +2,9 @@ describe "XBlock", -> ...@@ -2,9 +2,9 @@ describe "XBlock", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div> <div>
<div class='xblock' id='vA' data-runtime-version="A" data-init="initFnA" data-name="a-name"/> <div class='xblock' id='vA' data-runtime-version="A" data-runtime-class="TestRuntime" data-init="initFnA" data-name="a-name"/>
<div> <div>
<div class='xblock' id='vZ' data-runtime-version="Z" data-init="initFnZ"/> <div class='xblock' id='vZ' data-runtime-version="Z" data-runtime-class="TestRuntime" data-init="initFnZ"/>
</div> </div>
<div class='xblock' id='missing-version' data-init='initFnA' data-name='no-version'/> <div class='xblock' id='missing-version' data-init='initFnA' data-name='no-version'/>
<div class='xblock' id='missing-init' data-runtime-version="A" data-name='no-init'/> <div class='xblock' id='missing-init' data-runtime-version="A" data-name='no-init'/>
...@@ -13,8 +13,11 @@ describe "XBlock", -> ...@@ -13,8 +13,11 @@ describe "XBlock", ->
describe "initializeBlock", -> describe "initializeBlock", ->
beforeEach -> beforeEach ->
XBlock.runtime.vA = jasmine.createSpy().andReturn('runtimeA') window.TestRuntime = {}
XBlock.runtime.vZ = jasmine.createSpy().andReturn('runtimeZ') @runtimeA = {name: 'runtimeA'}
@runtimeZ = {name: 'runtimeZ'}
TestRuntime.vA = jasmine.createSpy().andReturn(@runtimeA)
TestRuntime.vZ = jasmine.createSpy().andReturn(@runtimeZ)
window.initFnA = jasmine.createSpy() window.initFnA = jasmine.createSpy()
window.initFnZ = jasmine.createSpy() window.initFnZ = jasmine.createSpy()
...@@ -28,12 +31,12 @@ describe "XBlock", -> ...@@ -28,12 +31,12 @@ describe "XBlock", ->
@missingInitBlock = XBlock.initializeBlock($('#missing-init')[0]) @missingInitBlock = XBlock.initializeBlock($('#missing-init')[0])
it "loads the right runtime version", -> it "loads the right runtime version", ->
expect(XBlock.runtime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren) expect(TestRuntime.vA).toHaveBeenCalledWith($('#vA')[0], @fakeChildren)
expect(XBlock.runtime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren) expect(TestRuntime.vZ).toHaveBeenCalledWith($('#vZ')[0], @fakeChildren)
it "loads the right init function", -> it "loads the right init function", ->
expect(window.initFnA).toHaveBeenCalledWith('runtimeA', $('#vA')[0]) expect(window.initFnA).toHaveBeenCalledWith(@runtimeA, $('#vA')[0])
expect(window.initFnZ).toHaveBeenCalledWith('runtimeZ', $('#vZ')[0]) expect(window.initFnZ).toHaveBeenCalledWith(@runtimeZ, $('#vZ')[0])
it "loads when missing versions", -> it "loads when missing versions", ->
expect(@missingVersionBlock.element).toBe($('#missing-version')) expect(@missingVersionBlock.element).toBe($('#missing-version'))
......
describe "XBlock.runtime.v1", -> describe "XBlock.Runtime.v1", ->
beforeEach -> beforeEach ->
setFixtures """ setFixtures """
<div class='xblock' data-handler-prefix='/xblock/fake-usage-id/handler'/> <div class='xblock' data-handler-prefix='/xblock/fake-usage-id/handler'/>
...@@ -10,9 +10,7 @@ describe "XBlock.runtime.v1", -> ...@@ -10,9 +10,7 @@ describe "XBlock.runtime.v1", ->
@element = $('.xblock')[0] @element = $('.xblock')[0]
@runtime = XBlock.runtime.v1(@element, @children) @runtime = new XBlock.Runtime.v1(@element, @children)
it "provides a handler url", ->
expect(@runtime.handlerUrl(@element, 'foo')).toBe('/xblock/fake-usage-id/handler/foo')
it "provides a list of children", -> it "provides a list of children", ->
expect(@runtime.children).toBe(@children) expect(@runtime.children).toBe(@children)
......
...@@ -6,7 +6,7 @@ from ddt import ddt, data ...@@ -6,7 +6,7 @@ from ddt import ddt, data
from mock import Mock from mock import Mock
from unittest import TestCase from unittest import TestCase
from urlparse import urlparse from urlparse import urlparse
from lms.lib.xblock.runtime import quote_slashes, unquote_slashes, handler_url from lms.lib.xblock.runtime import quote_slashes, unquote_slashes, LmsModuleSystem
TEST_STRINGS = [ TEST_STRINGS = [
'', '',
...@@ -41,23 +41,31 @@ class TestHandlerUrl(TestCase): ...@@ -41,23 +41,31 @@ class TestHandlerUrl(TestCase):
def setUp(self): def setUp(self):
self.block = Mock() self.block = Mock()
self.course_id = "org/course/run" self.course_id = "org/course/run"
self.runtime = LmsModuleSystem(
static_url='/static',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
replace_urls=str,
course_id=self.course_id,
)
def test_trailing_characters(self): def test_trailing_characters(self):
self.assertFalse(handler_url(self.course_id, self.block, 'handler').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix', 'query').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix', 'query').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', 'suffix', 'query').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', 'suffix', 'query').endswith('/'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', query='query').endswith('?')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', query='query').endswith('?'))
self.assertFalse(handler_url(self.course_id, self.block, 'handler', query='query').endswith('/')) self.assertFalse(self.runtime.handler_url(self.block, 'handler', query='query').endswith('/'))
def _parsed_query(self, query_string): def _parsed_query(self, query_string):
"""Return the parsed query string from a handler_url generated with the supplied query_string""" """Return the parsed query string from a handler_url generated with the supplied query_string"""
return urlparse(handler_url(self.course_id, self.block, 'handler', query=query_string)).query return urlparse(self.runtime.handler_url(self.block, 'handler', query=query_string)).query
def test_query_string(self): def test_query_string(self):
self.assertIn('foo=bar', self._parsed_query('foo=bar')) self.assertIn('foo=bar', self._parsed_query('foo=bar'))
...@@ -66,7 +74,7 @@ class TestHandlerUrl(TestCase): ...@@ -66,7 +74,7 @@ class TestHandlerUrl(TestCase):
def _parsed_path(self, handler_name='handler', suffix=''): def _parsed_path(self, handler_name='handler', suffix=''):
"""Return the parsed path from a handler_url with the supplied handler_name and suffix""" """Return the parsed path from a handler_url with the supplied handler_name and suffix"""
return urlparse(handler_url(self.course_id, self.block, handler_name, suffix=suffix)).path return urlparse(self.runtime.handler_url(self.block, handler_name, suffix=suffix)).path
def test_suffix(self): def test_suffix(self):
self.assertTrue(self._parsed_path(suffix="foo").endswith('foo')) self.assertTrue(self._parsed_path(suffix="foo").endswith('foo'))
......
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