Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
a79d79aa
Commit
a79d79aa
authored
Dec 12, 2013
by
Anton Stupak
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1891 from edx/anton/fix-lti-dnd
LTI: fix reordering bug in Studio
parents
0b161998
2ea16631
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
87 additions
and
709 deletions
+87
-709
common/lib/xmodule/xmodule/css/lti/lti.scss
+0
-1
common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
+0
-370
common/lib/xmodule/xmodule/js/src/lti/01_lti.js
+0
-192
common/lib/xmodule/xmodule/js/src/lti/02_main.js
+0
-57
common/lib/xmodule/xmodule/lti_module.py
+23
-18
common/lib/xmodule/xmodule/tests/test_lti_unit.py
+6
-22
lms/djangoapps/courseware/features/lti.py
+18
-17
lms/djangoapps/courseware/tests/test_lti_integration.py
+1
-4
lms/templates/lti.html
+3
-28
lms/templates/lti_form.html
+36
-0
No files found.
common/lib/xmodule/xmodule/css/lti/lti.scss
View file @
a79d79aa
...
...
@@ -30,6 +30,5 @@ div.lti {
height
:
800px
;
display
:
block
;
border
:
0px
;
overflow-x
:
hidden
;
}
}
common/lib/xmodule/xmodule/js/spec/lti/lti_spec.js
deleted
100644 → 0
View file @
0b161998
/**
* File: constructor.js
*
* Purpose: Jasmine tests for LTI module (front-end part).
*
*
* Because LTI module is constructed so that all methods are available via the
* prototype chain, many times we can test methods without having to
* instantiate a new LTI object.
*/
/*
* "Hence that general is skillful in attack whose opponent does not know what
* to defend; and he is skillful in defense whose opponent does not know what
* to attack."
*
* ~ Sun Tzu
*/
(
function
()
{
var
IN_NEW_WINDOW
=
'true'
,
IN_IFRAME
=
'false'
,
EMPTY_URL
=
''
,
DEFAULT_URL
=
'http://www.example.com'
,
NEW_URL
=
'http://www.example.com/some_book'
;
describe
(
'LTI XModule'
,
function
()
{
describe
(
'LTIConstructor method'
,
function
()
{
describe
(
'[in iframe, new url]'
,
function
()
{
var
lti
;
beforeEach
(
function
()
{
loadFixtures
(
'lti.html'
);
setUpLtiElement
(
$
(
'.lti-wrapper'
),
IN_IFRAME
,
NEW_URL
);
spyOnEvent
(
$
(
'.lti-wrapper'
).
find
(
'.ltiLaunchForm'
),
'submit'
);
lti
=
new
window
.
LTI
(
'.lti-wrapper'
);
});
it
(
'new LTI object contains all properties'
,
function
()
{
expect
(
lti
.
el
).
toBeDefined
();
expect
(
lti
.
el
).
toExist
();
expect
(
lti
.
formEl
).
toBeDefined
();
expect
(
lti
.
formEl
).
toExist
();
expect
(
lti
.
formEl
).
toHaveAttr
(
'action'
);
expect
(
lti
.
ltiEl
).
toBeDefined
();
expect
(
lti
.
ltiEl
).
toExist
();
expect
(
lti
.
formAction
).
toEqual
(
NEW_URL
);
expect
(
lti
.
openInANewPage
).
toEqual
(
false
);
expect
(
lti
.
ajaxUrl
).
toEqual
(
jasmine
.
any
(
String
));
expect
(
'submit'
).
toHaveBeenTriggeredOn
(
lti
.
formEl
);
});
afterEach
(
function
()
{
lti
=
undefined
;
});
});
describe
(
'[in new window, new url]'
,
function
()
{
var
lti
;
beforeEach
(
function
()
{
loadFixtures
(
'lti.html'
);
setUpLtiElement
(
$
(
'.lti-wrapper'
),
IN_NEW_WINDOW
,
NEW_URL
);
lti
=
new
window
.
LTI
(
'.lti-wrapper'
);
});
it
(
'check extra properties and values'
,
function
()
{
expect
(
lti
.
openInANewPage
).
toEqual
(
true
);
expect
(
lti
.
signatureIsNew
).
toBeTruthy
();
expect
(
lti
.
newWindowBtnEl
).
toBeDefined
();
expect
(
lti
.
newWindowBtnEl
).
toExist
();
expect
(
lti
.
disableOpenNewWindowBtn
).
toBe
(
false
);
});
afterEach
(
function
()
{
lti
=
undefined
;
});
});
describe
(
'[in iframe, NO new url]'
,
function
()
{
var
testCases
=
[{
itDescription
:
'URL is blank'
,
action
:
EMPTY_URL
},
{
itDescription
:
'URL is default'
,
action
:
DEFAULT_URL
}];
$
.
each
(
testCases
,
function
(
index
,
test
)
{
it
(
test
.
itDescription
,
function
()
{
var
lti
;
loadFixtures
(
'lti.html'
);
setUpLtiElement
(
$
(
'.lti-wrapper'
),
IN_IFRAME
,
test
.
action
);
lti
=
new
window
.
LTI
(
'.lti-wrapper'
);
expect
(
lti
.
openInANewPage
).
not
.
toBeDefined
();
});
});
});
});
describe
(
'submitFormHandler method'
,
function
()
{
var
thisObj
;
beforeEach
(
function
()
{
thisObj
=
{
signatureIsNew
:
undefined
,
getNewSignature
:
jasmine
.
createSpy
(
'getNewSignature'
),
formEl
:
{
submit
:
jasmine
.
createSpy
(
'submit'
)
}
};
});
it
(
'signature is new'
,
function
()
{
thisObj
.
signatureIsNew
=
true
;
window
.
LTI
.
prototype
.
submitFormHandler
.
call
(
thisObj
);
expect
(
thisObj
.
formEl
.
submit
).
toHaveBeenCalled
();
expect
(
thisObj
.
signatureIsNew
).
toBe
(
false
);
});
it
(
'signature is old'
,
function
()
{
thisObj
.
signatureIsNew
=
false
;
window
.
LTI
.
prototype
.
submitFormHandler
.
call
(
thisObj
);
expect
(
thisObj
.
formEl
.
submit
).
not
.
toHaveBeenCalled
();
expect
(
thisObj
.
signatureIsNew
).
toBe
(
false
);
expect
(
thisObj
.
getNewSignature
).
toHaveBeenCalled
();
});
afterEach
(
function
()
{
thisObj
=
undefined
;
});
});
describe
(
'getNewSignature method'
,
function
()
{
var
lti
;
beforeEach
(
function
()
{
loadFixtures
(
'lti.html'
);
setUpLtiElement
(
$
(
'.lti-wrapper'
),
IN_NEW_WINDOW
,
NEW_URL
);
spyOn
(
$
,
'postWithPrefix'
).
andCallFake
(
function
(
url
,
data
,
callback
)
{
callback
({
input_fields
:
{}
});
}
);
lti
=
new
window
.
LTI
(
'.lti-wrapper'
);
spyOn
(
lti
,
'submitFormHandler'
).
andCallThrough
();
lti
.
submitFormHandler
.
reset
();
spyOn
(
lti
,
'handleAjaxUpdateSignature'
);
});
it
(
'"Open in new page" clicked twice, signature requested once'
,
function
()
{
lti
.
newWindowBtnEl
.
click
();
lti
.
newWindowBtnEl
.
click
();
expect
(
lti
.
submitFormHandler
).
toHaveBeenCalled
();
expect
(
lti
.
submitFormHandler
.
callCount
).
toBe
(
2
);
expect
(
$
.
postWithPrefix
).
toHaveBeenCalledWith
(
lti
.
ajaxUrl
+
'/regenerate_signature'
,
{},
jasmine
.
any
(
Function
)
);
expect
(
lti
.
disableOpenNewWindowBtn
).
toBe
(
true
);
expect
(
lti
.
handleAjaxUpdateSignature
)
.
toHaveBeenCalledWith
({
input_fields
:
{}
});
}
);
afterEach
(
function
()
{
lti
=
undefined
;
});
});
describe
(
'handleAjaxUpdateSignature method'
,
function
()
{
var
lti
,
oldInputFields
,
newInputFields
,
AjaxCallbackData
=
{};
function
fakePostWithPrefix
(
url
,
data
,
callback
)
{
return
callback
(
AjaxCallbackData
);
}
beforeEach
(
function
()
{
oldInputFields
=
{
oauth_nonce
:
'28347958723982798572'
,
oauth_timestamp
:
'2389479832'
,
oauth_signature
:
'89ru3289r3ry283y3r82ryr38yr'
};
newInputFields
=
{
oauth_nonce
:
'ru3902ru239ru'
,
oauth_timestamp
:
'24ru309rur39r8u'
,
oauth_signature
:
'08923ru3082u2rur'
};
AjaxCallbackData
.
error
=
0
;
AjaxCallbackData
.
input_fields
=
newInputFields
;
loadFixtures
(
'lti.html'
);
setUpLtiElement
(
$
(
'.lti-wrapper'
),
IN_NEW_WINDOW
,
NEW_URL
);
spyOn
(
$
,
'postWithPrefix'
).
andCallFake
(
fakePostWithPrefix
);
lti
=
new
window
.
LTI
(
'.lti-wrapper'
);
spyOn
(
lti
,
'submitFormHandler'
).
andCallThrough
();
spyOn
(
lti
,
'handleAjaxUpdateSignature'
).
andCallThrough
();
spyOn
(
lti
.
formEl
,
'submit'
);
spyOn
(
window
.
console
,
'log'
).
andCallThrough
();
lti
.
submitFormHandler
.
reset
();
lti
.
handleAjaxUpdateSignature
.
reset
();
lti
.
formEl
.
submit
.
reset
();
window
.
console
.
log
.
reset
();
});
it
(
'On second click form is updated, and submitted'
,
function
()
{
// Setup initial OAuth values in the form.
lti
.
formEl
.
find
(
"input[name='oauth_nonce']"
)
.
val
(
oldInputFields
.
oauth_nonce
);
lti
.
formEl
.
find
(
"input[name='oauth_timestamp']"
)
.
val
(
oldInputFields
.
oauth_timestamp
);
lti
.
formEl
.
find
(
"input[name='oauth_signature']"
)
.
val
(
oldInputFields
.
oauth_signature
);
// First click. Signature is new. Should just submit the form.
lti
.
newWindowBtnEl
.
click
();
// Initial OAuth values should not have changed.
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_nonce']"
).
val
())
.
toBe
(
oldInputFields
.
oauth_nonce
);
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_timestamp']"
).
val
())
.
toBe
(
oldInputFields
.
oauth_timestamp
);
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_signature']"
).
val
())
.
toBe
(
oldInputFields
.
oauth_signature
);
expect
(
lti
.
submitFormHandler
).
toHaveBeenCalled
();
expect
(
lti
.
submitFormHandler
.
callCount
).
toBe
(
1
);
expect
(
lti
.
handleAjaxUpdateSignature
).
not
.
toHaveBeenCalled
();
expect
(
lti
.
handleAjaxUpdateSignature
.
callCount
).
toBe
(
0
);
expect
(
lti
.
formEl
.
submit
).
toHaveBeenCalled
();
expect
(
lti
.
formEl
.
submit
.
callCount
).
toBe
(
1
);
lti
.
submitFormHandler
.
reset
();
lti
.
handleAjaxUpdateSignature
.
reset
();
lti
.
formEl
.
submit
.
reset
();
// Second click. Signature is old. Should request for a new
// signature, and then submit the form.
lti
.
newWindowBtnEl
.
click
();
expect
(
lti
.
submitFormHandler
).
toHaveBeenCalled
();
expect
(
lti
.
submitFormHandler
.
callCount
).
toBe
(
2
);
expect
(
lti
.
handleAjaxUpdateSignature
).
toHaveBeenCalled
();
expect
(
lti
.
handleAjaxUpdateSignature
.
callCount
).
toBe
(
1
);
expect
(
lti
.
formEl
.
submit
).
toHaveBeenCalled
();
expect
(
lti
.
formEl
.
submit
.
callCount
).
toBe
(
1
);
expect
(
lti
.
disableOpenNewWindowBtn
).
toBe
(
false
);
// The new OAuth values should be in the form.
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_nonce']"
).
val
())
.
toBe
(
newInputFields
.
oauth_nonce
);
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_timestamp']"
).
val
())
.
toBe
(
newInputFields
.
oauth_timestamp
);
expect
(
lti
.
formEl
.
find
(
"input[name='oauth_signature']"
).
val
())
.
toBe
(
newInputFields
.
oauth_signature
);
});
it
(
'invalid response for new OAuth signature'
,
function
()
{
AjaxCallbackData
.
input_fields
=
0
;
AjaxCallbackData
.
error
=
'error'
;
lti
.
newWindowBtnEl
.
click
();
lti
.
submitFormHandler
.
reset
();
lti
.
handleAjaxUpdateSignature
.
reset
();
window
.
console
.
log
.
reset
();
lti
.
formEl
.
submit
.
reset
();
lti
.
newWindowBtnEl
.
click
();
expect
(
lti
.
submitFormHandler
).
toHaveBeenCalled
();
expect
(
lti
.
submitFormHandler
.
callCount
).
toBe
(
1
);
expect
(
lti
.
handleAjaxUpdateSignature
).
toHaveBeenCalled
();
expect
(
lti
.
handleAjaxUpdateSignature
.
callCount
).
toBe
(
1
);
expect
(
window
.
console
.
log
).
toHaveBeenCalledWith
(
jasmine
.
any
(
String
)
);
expect
(
lti
.
formEl
.
submit
).
not
.
toHaveBeenCalled
();
});
afterEach
(
function
()
{
lti
=
undefined
;
oldInputFields
=
undefined
;
newInputFields
=
undefined
;
});
});
});
function
setUpLtiElement
(
element
,
target
,
action
)
{
var
container
,
form
;
container
=
element
.
find
(
'.lti'
);
form
=
container
.
find
(
'.ltiLaunchForm'
);
if
(
target
===
IN_IFRAME
)
{
container
.
data
(
'open_in_a_new_page'
,
'false'
);
form
.
attr
(
'target'
,
'ltiLaunchFrame'
);
}
form
.
attr
(
'action'
,
action
);
// If we have a new proper action (non-default), we create either
// a link that will submit the form, or an iframe that will contain
// the answer of auto submitted form.
if
(
action
!==
EMPTY_URL
&&
action
!==
DEFAULT_URL
)
{
if
(
target
===
IN_NEW_WINDOW
)
{
$
(
'<a />'
,
{
href
:
'#'
,
class
:
'link_lti_new_window'
}).
appendTo
(
container
);
}
else
{
$
(
'<iframe />'
,
{
name
:
'ltiLaunchFrame'
,
class
:
'ltiLaunchFrame'
,
src
:
''
}).
appendTo
(
container
);
}
}
}
}());
common/lib/xmodule/xmodule/js/src/lti/01_lti.js
deleted
100644 → 0
View file @
0b161998
/**
* File: lti.js
*
* Purpose: LTI module constructor. Given an LTI element, we process it.
*
*
* Inside the element there is a form. If that form has a valid action
* attribute, then we do one of:
*
* 1.) Submit the form. The results will be shown on the current page in an
* iframe.
* 2.) Attach a handler function to a link which will submit the form. The
* results will be shown in a new window.
*
* The 'open_in_a_new_page' data attribute of the LTI element dictates which of
* the two actions will be performed.
*/
/*
* So the thing to do when working on a motorcycle, as in any other task, is to
* cultivate the peace of mind which does not separate one's self from one's
* surroundings. When that is done successfully, then everything else follows
* naturally. Peace of mind produces right values, right values produce right
* thoughts. Right thoughts produce right actions and right actions produce
* work which will be a material reflection for others to see of the serenity
* at the center of it all.
*
* ~ Robert M. Pirsig
*/
(
function
(
requirejs
,
require
,
define
)
{
// JavaScript LTI XModule
define
(
'lti/01_lti.js'
,
[],
function
()
{
var
LTI
=
LTIConstructor
;
LTI
.
prototype
=
{
submitFormHandler
:
submitFormHandler
,
getNewSignature
:
getNewSignature
,
handleAjaxUpdateSignature
:
handleAjaxUpdateSignature
};
return
LTI
;
// JavaScript LTI XModule constructor
function
LTIConstructor
(
element
)
{
var
_this
=
this
;
// In cms (Studio) the element is already a jQuery object. In lms it is
// a DOM object.
//
// To make sure that there is no error, we pass it through the $()
// function. This will make it a jQuery object if it isn't already so.
this
.
el
=
$
(
element
);
this
.
formEl
=
this
.
el
.
find
(
'.ltiLaunchForm'
);
this
.
formAction
=
this
.
formEl
.
attr
(
'action'
);
// If action is empty string, or action is the default URL that should
// not cause a form submit.
if
(
!
this
.
formAction
||
this
.
formAction
===
'http://www.example.com'
)
{
// Nothing to do - no valid action provided. Error message will be
// displaced in browser (HTML).
return
;
}
this
.
ltiEl
=
this
.
el
.
find
(
'.lti'
);
// We want a Boolean 'true' or 'false'. First we will retrieve the data
// attribute.
this
.
openInANewPage
=
this
.
ltiEl
.
data
(
'open_in_a_new_page'
);
// Then we will parse it via native JSON.parse().
this
.
openInANewPage
=
JSON
.
parse
(
this
.
openInANewPage
);
// The URL where we can request for a new OAuth signature for form
// submission to the LTI provider.
this
.
ajaxUrl
=
this
.
ltiEl
.
data
(
'ajax_url'
);
// The OAuth signature can only be used once (because of timestamp
// and nonce). This will be reset each time the form is submitted so
// that we know to fetch a new OAuth signature on subsequent form
// submit.
this
.
signatureIsNew
=
true
;
// If the Form's action attribute is set (i.e. we can perform a normal
// submit), then we (depending on instance settings) submit the form
// when user will click on a link, or submit the form immediately.
if
(
this
.
openInANewPage
===
true
)
{
// From the start, the button is enabled.
this
.
disableOpenNewWindowBtn
=
false
;
this
.
newWindowBtnEl
=
this
.
el
.
find
(
'.link_lti_new_window'
)
.
on
(
'click'
,
function
()
{
// Don't allow clicking repeatedly on this button
// if we are waiting for an AJAX response (with new
// OAuth signature).
if
(
_this
.
disableOpenNewWindowBtn
===
true
)
{
return
;
}
return
_this
.
submitFormHandler
();
}
);
}
else
{
// At this stage the form exists on the page and has a valid
// action. We are safe to submit it, even if `openInANewPage` is
// set to some weird value.
this
.
submitFormHandler
();
}
}
// The form submit handler. Before the form is submitted, we must check if
// the OAuth signature is new (valid). If it is not new, block form
// submission and request for a signature. After a new signature is
// fetched, the form will be submitted.
function
submitFormHandler
()
{
if
(
this
.
signatureIsNew
)
{
// Continue with submitting the form.
this
.
formEl
.
submit
();
// If the OAuth signature is new, mark it as old.
this
.
signatureIsNew
=
false
;
// If we have an "Open LTI in a new window" button.
if
(
this
.
newWindowBtnEl
)
{
// Enable clicking on the button again.
this
.
disableOpenNewWindowBtn
=
false
;
}
}
else
{
// The OAuth signature is old. Request for a new OAuth signature.
//
// Don't submit the form. It will be submitted once a new OAuth
// signature is received.
this
.
getNewSignature
();
}
}
// Request form the server a new OAuth signature.
function
getNewSignature
()
{
var
_this
=
this
;
// If we have an "Open LTI in a new window" button.
if
(
this
.
newWindowBtnEl
)
{
// Make sure that while we are waiting for a new signature, the
// user can't click on the "Open LTI in a new window" button
// repeatedly.
this
.
disableOpenNewWindowBtn
=
true
;
}
$
.
postWithPrefix
(
this
.
ajaxUrl
+
'/regenerate_signature'
,
{},
function
(
response
)
{
return
_this
.
handleAjaxUpdateSignature
(
response
);
}
);
}
// When a new OAuth signature is received, and if the data received back is
// OK, update the form, and submit it.
function
handleAjaxUpdateSignature
(
response
)
{
var
_this
=
this
;
// If the response is valid, and contains expected data.
if
(
$
.
isPlainObject
(
response
.
input_fields
))
{
// We received a new OAuth signature.
this
.
signatureIsNew
=
true
;
// Update the form fields with new data, and new OAuth
// signature.
$
.
each
(
response
.
input_fields
,
function
(
name
,
value
)
{
var
inputEl
=
_this
.
formEl
.
find
(
"input[name='"
+
name
+
"']"
);
inputEl
.
val
(
value
);
});
// Submit the form.
this
.
submitFormHandler
();
}
else
{
console
.
log
(
'[LTI info]: '
+
response
.
error
);
}
}
});
}(
RequireJS
.
requirejs
,
RequireJS
.
require
,
RequireJS
.
define
));
common/lib/xmodule/xmodule/js/src/lti/02_main.js
deleted
100644 → 0
View file @
0b161998
(
function
(
requirejs
,
require
,
define
)
{
// In the case when the LTI constructor will be called before
// RequireJS finishes loading all of the LTI dependencies, we will have
// a mock function that will collect all the elements that must be
// initialized as LTI elements.
//
// Once RequireJS will load all of the necessary dependencies, main code
// will invoke the mock function with the second parameter set to truthy value.
// This will trigger the actual LTI constructor on all elements that
// are stored in a temporary list.
window
.
LTI
=
(
function
()
{
// Temporary storage place for elements that must be initialized as LTI
// elements.
var
tempCallStack
=
[];
return
function
(
element
,
processTempCallStack
)
{
// If mock function was called with second parameter set to truthy
// value, we invoke the real `window.LTI` on all the stored elements
// so far.
if
(
processTempCallStack
)
{
$
.
each
(
tempCallStack
,
function
(
index
,
element
)
{
// By now, `window.LTI` is the real constructor.
window
.
LTI
(
element
);
});
return
;
}
// If normal call to `window.LTI` constructor, store the element
// for later initializing.
tempCallStack
.
push
(
element
);
// Real LTI constructor returns `undefined`. The mock constructor will
// return the same value. Making this explicit.
return
undefined
;
};
}());
// Main module.
require
(
[
'lti/01_lti.js'
],
function
(
LTIConstructor
)
{
var
oldLTI
=
window
.
LTI
;
window
.
LTI
=
LTIConstructor
;
// Invoke the mock LTI constructor so that the elements stored within
// it can be processed by the real `window.LTI` constructor.
oldLTI
(
null
,
true
);
});
}(
RequireJS
.
requirejs
,
RequireJS
.
require
,
RequireJS
.
define
));
common/lib/xmodule/xmodule/lti_module.py
View file @
a79d79aa
...
...
@@ -180,14 +180,7 @@ class LTIModule(LTIFields, XModule):
Otherwise error message from LTI provider is generated.
"""
js
=
{
'js'
:
[
resource_string
(
__name__
,
'js/src/lti/01_lti.js'
),
resource_string
(
__name__
,
'js/src/lti/02_main.js'
)
]
}
css
=
{
'scss'
:
[
resource_string
(
__name__
,
'css/lti/lti.scss'
)]}
js_module_name
=
"LTI"
def
get_input_fields
(
self
):
# LTI provides a list of default parameters that might be passed as
...
...
@@ -253,12 +246,11 @@ class LTIModule(LTIFields, XModule):
client_secret
,
)
def
get_
html
(
self
):
def
get_
context
(
self
):
"""
Re
nders parameters to template
.
Re
turns a context
.
"""
context
=
{
return
{
'input_fields'
:
self
.
get_input_fields
(),
# These parameters do not participate in OAuth signing.
...
...
@@ -267,12 +259,27 @@ class LTIModule(LTIFields, XModule):
'element_class'
:
self
.
category
,
'open_in_a_new_page'
:
self
.
open_in_a_new_page
,
'display_name'
:
self
.
display_name
,
'
ajax_url'
:
self
.
system
.
ajax_url
,
'
form_url'
:
self
.
get_form_path
()
,
}
return
self
.
system
.
render_template
(
'lti.html'
,
context
)
def
handle_ajax
(
self
,
dispatch
,
__
):
def
get_form_path
(
self
):
return
self
.
runtime
.
handler_url
(
self
,
'preview_handler'
)
.
rstrip
(
'/?'
)
def
get_html
(
self
):
"""
Renders parameters to template.
"""
return
self
.
system
.
render_template
(
'lti.html'
,
self
.
get_context
())
def
get_form
(
self
):
"""
Renders parameters to form template.
"""
return
self
.
system
.
render_template
(
'lti_form.html'
,
self
.
get_context
())
@XBlock.handler
def
preview_handler
(
self
,
request
,
dispatch
):
"""
Ajax handler.
...
...
@@ -282,10 +289,7 @@ class LTIModule(LTIFields, XModule):
Returns:
json string
"""
if
dispatch
==
'regenerate_signature'
:
return
json
.
dumps
({
'input_fields'
:
self
.
get_input_fields
()
})
else
:
# return error message
return
json
.
dumps
({
'error'
:
'[handle_ajax]: Unknown Command!'
})
return
Response
(
self
.
get_form
(),
content_type
=
'text/html'
)
def
get_user_id
(
self
):
user_id
=
self
.
runtime
.
anonymous_student_id
...
...
@@ -614,3 +618,4 @@ class LTIDescriptor(LTIFields, MetadataOnlyEditingDescriptor, EmptyDataRawDescri
"""
module_class
=
LTIModule
grade_handler
=
module_attr
(
'grade_handler'
)
preview_handler
=
module_attr
(
'preview_handler'
)
common/lib/xmodule/xmodule/tests/test_lti_unit.py
View file @
a79d79aa
...
...
@@ -229,6 +229,12 @@ class LTIModuleTest(LogicTest):
real_outcome_service_url
=
self
.
xmodule
.
get_outcome_service_url
()
self
.
assertEqual
(
real_outcome_service_url
,
expected_outcome_service_url
)
def
test_get_form_path
(
self
):
expected_form_path
=
self
.
xmodule
.
runtime
.
handler_url
(
self
.
xmodule
,
'preview_handler'
)
.
rstrip
(
'/?'
)
real_form_path
=
self
.
xmodule
.
get_form_path
()
self
.
assertEqual
(
real_form_path
,
expected_form_path
)
def
test_resource_link_id
(
self
):
with
patch
(
'xmodule.lti_module.LTIModule.id'
,
new_callable
=
PropertyMock
)
as
mock_id
:
mock_id
.
return_value
=
self
.
module_id
...
...
@@ -251,28 +257,6 @@ class LTIModuleTest(LogicTest):
def
test_client_key_secret
(
self
):
pass
def
test_handle_ajax
(
self
):
dispatch
=
'regenerate_signature'
data
=
''
self
.
xmodule
.
get_input_fields
=
Mock
(
return_value
=
{
'test_input_field_key'
:
'test_input_field_value'
})
json_dump
=
self
.
xmodule
.
handle_ajax
(
dispatch
,
data
)
expected_json_dump
=
'{"input_fields": {"test_input_field_key": "test_input_field_value"}}'
self
.
assertEqual
(
json
.
loads
(
json_dump
),
json
.
loads
(
expected_json_dump
)
)
def
test_handle_ajax_bad_dispatch
(
self
):
dispatch
=
'bad_dispatch'
data
=
''
self
.
xmodule
.
get_input_fields
=
Mock
(
return_value
=
{
'test_input_field_key'
:
'test_input_field_value'
})
json_dump
=
self
.
xmodule
.
handle_ajax
(
dispatch
,
data
)
expected_json_dump
=
'{"error": "[handle_ajax]: Unknown Command!"}'
self
.
assertEqual
(
json
.
loads
(
json_dump
),
json
.
loads
(
expected_json_dump
)
)
def
test_max_score
(
self
):
self
.
xmodule
.
weight
=
100.0
...
...
lms/djangoapps/courseware/features/lti.py
View file @
a79d79aa
...
...
@@ -13,22 +13,22 @@ from courseware.tests.factories import InstructorFactory
@step
(
'I view the LTI and error is shown$'
)
def
lti_is_not_rendered
(
_step
):
# error is shown
assert
world
.
is_css_present
(
'.error_message'
)
assert
world
.
is_css_present
(
'.error_message'
,
wait_time
=
0
)
# iframe is not presented
assert
not
world
.
is_css_present
(
'iframe'
)
assert
not
world
.
is_css_present
(
'iframe'
,
wait_time
=
0
)
# link is not presented
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
)
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
,
wait_time
=
0
)
def
check_lti_iframe_content
(
text
):
#inside iframe test content is presented
location
=
world
.
scenario_dict
[
'LTI'
]
.
location
.
html_id
()
iframe_name
=
'lti
Launch
Frame-'
+
location
iframe_name
=
'ltiFrame-'
+
location
with
world
.
browser
.
get_iframe
(
iframe_name
)
as
iframe
:
# iframe does not contain functions from terrain/ui_helpers.py
assert
iframe
.
is_element_present_by_css
(
'.result'
,
wait_time
=
5
)
assert
iframe
.
is_element_present_by_css
(
'.result'
,
wait_time
=
0
)
assert
(
text
==
world
.
retry_on_exception
(
lambda
:
iframe
.
find_by_css
(
'.result'
)[
0
]
.
text
,
max_attempts
=
5
...
...
@@ -38,18 +38,18 @@ def check_lti_iframe_content(text):
@step
(
'I view the LTI and it is rendered in (.*)$'
)
def
lti_is_rendered
(
_step
,
rendered_in
):
if
rendered_in
.
strip
()
==
'iframe'
:
assert
world
.
is_css_present
(
'iframe'
)
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
)
assert
not
world
.
is_css_present
(
'.error_message'
)
assert
world
.
is_css_present
(
'iframe'
,
wait_time
=
2
)
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
,
wait_time
=
0
)
assert
not
world
.
is_css_present
(
'.error_message'
,
wait_time
=
0
)
# iframe is visible
assert
world
.
css_visible
(
'iframe'
)
check_lti_iframe_content
(
"This is LTI tool. Success."
)
elif
rendered_in
.
strip
()
==
'new page'
:
assert
not
world
.
is_css_present
(
'iframe'
)
assert
world
.
is_css_present
(
'.link_lti_new_window'
)
assert
not
world
.
is_css_present
(
'.error_message'
)
assert
not
world
.
is_css_present
(
'iframe'
,
wait_time
=
2
)
assert
world
.
is_css_present
(
'.link_lti_new_window'
,
wait_time
=
0
)
assert
not
world
.
is_css_present
(
'.error_message'
,
wait_time
=
0
)
check_lti_popup
()
else
:
# incorrent rendered_in parameter
assert
False
...
...
@@ -57,9 +57,9 @@ def lti_is_rendered(_step, rendered_in):
@step
(
'I view the LTI but incorrect_signature warning is rendered$'
)
def
incorrect_lti_is_rendered
(
_step
):
assert
world
.
is_css_present
(
'iframe'
)
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
)
assert
not
world
.
is_css_present
(
'.error_message'
)
assert
world
.
is_css_present
(
'iframe'
,
wait_time
=
2
)
assert
not
world
.
is_css_present
(
'.link_lti_new_window'
,
wait_time
=
0
)
assert
not
world
.
is_css_present
(
'.error_message'
,
wait_time
=
0
)
#inside iframe test content is presented
check_lti_iframe_content
(
"Wrong LTI signature"
)
...
...
@@ -234,10 +234,11 @@ def check_progress(_step, text):
@step
(
'I see graph with total progress "([^"]*)"$'
)
def
see_graph
(
_step
,
progress
):
SELECTOR
=
'grade-detail-graph'
node
=
world
.
browser
.
find_by_xpath
(
'//div[@id="{parent}"]//div[text()="{progress}"]'
.
format
(
XPATH
=
'//div[@id="{parent}"]//div[text()="{progress}"]'
.
format
(
parent
=
SELECTOR
,
progress
=
progress
,
))
)
node
=
world
.
browser
.
find_by_xpath
(
XPATH
)
assert
node
...
...
@@ -259,7 +260,7 @@ def see_value_in_the_gradebook(_step, label, text):
@step
(
'I submit answer to LTI question$'
)
def
click_grade
(
_step
):
location
=
world
.
scenario_dict
[
'LTI'
]
.
location
.
html_id
()
iframe_name
=
'lti
Launch
Frame-'
+
location
iframe_name
=
'ltiFrame-'
+
location
with
world
.
browser
.
get_iframe
(
iframe_name
)
as
iframe
:
iframe
.
find_by_name
(
'submit-button'
)
.
first
.
click
()
assert
iframe
.
is_text_present
(
'LTI consumer (edX) responded with XML content'
)
...
...
lms/djangoapps/courseware/tests/test_lti_integration.py
View file @
a79d79aa
...
...
@@ -5,8 +5,6 @@ from . import BaseTestXmodule
from
collections
import
OrderedDict
import
mock
import
urllib
from
xmodule.lti_module
import
LTIModule
from
mock
import
Mock
class
TestLTI
(
BaseTestXmodule
):
...
...
@@ -85,7 +83,6 @@ class TestLTI(BaseTestXmodule):
Makes sure that all parameters extracted.
"""
generated_context
=
self
.
item_module
.
render
(
'student_view'
)
.
content
expected_context
=
{
'display_name'
:
self
.
item_module
.
display_name
,
'input_fields'
:
self
.
correct_headers
,
...
...
@@ -93,7 +90,7 @@ class TestLTI(BaseTestXmodule):
'element_id'
:
self
.
item_module
.
location
.
html_id
(),
'launch_url'
:
'http://www.example.com'
,
# default value
'open_in_a_new_page'
:
True
,
'
ajax_url'
:
self
.
item_descriptor
.
xmodule_runtime
.
ajax_url
,
'
form_url'
:
self
.
item_descriptor
.
xmodule_runtime
.
handler_url
(
self
.
item_module
,
'preview_handler'
)
.
rstrip
(
'/?'
)
,
}
self
.
assertEqual
(
...
...
lms/templates/lti.html
View file @
a79d79aa
...
...
@@ -4,40 +4,15 @@
<div
id=
"${element_id}"
class=
"${element_class}"
data-open_in_a_new_page=
"${json.dumps(open_in_a_new_page)}"
data-ajax_url=
"${ajax_url}"
>
## This form will be hidden.
## If open_in_a_new_page is false then, once available on the client, the
## LTI module JavaScript will trigger a "submit" on the form, and the
## result will be rendered to the below iFrame.
## If open_in_a_new_page is true, then link will be shown, and by clicking
## on it, LTI will pop up in new window.
<form
action=
"${launch_url}"
name=
"ltiLaunchForm-${element_id}"
class=
"ltiLaunchForm"
method=
"post"
target=
${"_blank"
if
open_in_a_new_page
else
"
ltiLaunchFrame-
{
0
}".
format
(
element_id
)}
encType=
"application/x-www-form-urlencoded"
>
% for param_name, param_value in input_fields.items():
<input
name=
"${param_name}"
value=
"${param_value}"
/>
%endfor
<input
type=
"submit"
value=
"Press to Launch"
/>
</form>
% if launch_url and launch_url != 'http://www.example.com':
% if open_in_a_new_page:
<div
class=
"wrapper-lti-link"
>
<h3
class=
"title"
>
${display_name} (${_('External resource')})
</h3>
<p
class=
"lti-link external"
><a
href=
"#"
class=
'link_lti_new_window'
>
<p
class=
"lti-link external"
><a
target=
"_blank"
class=
"link_lti_new_window"
href=
"${form_url}"
>
${_('View resource in a new window')}
<i
class=
"icon-external-link"
></i>
</a></p>
...
...
@@ -45,9 +20,9 @@
% else:
## The result of the form submit will be rendered here.
<iframe
name=
"ltiLaunchFrame-${element_id}"
class=
"ltiLaunchFrame"
src=
""
name=
"ltiFrame-${element_id}"
src=
"${form_url}"
></iframe>
% endif
% else:
...
...
lms/templates/lti_form.html
0 → 100644
View file @
a79d79aa
<
%!
import
json
%
>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<!DOCTYPE HTML>
<html>
<head>
<meta
http-equiv=
"Content-Type"
content=
"text/html; charset=UTF-8"
/>
<title>
LTI
</title>
</head>
<body>
## This form will be hidden.
## LTI module JavaScript will trigger a "submit" on the form, and the
## result will be rendered instead.
<form
id=
"lti-${element_id}"
action=
"${launch_url}"
method=
"post"
encType=
"application/x-www-form-urlencoded"
style=
"display:none;"
>
% for param_name, param_value in input_fields.items():
<input
name=
"${param_name}"
value=
"${param_value}"
/>
%endfor
<input
type=
"submit"
value=
"Press to Launch"
/>
</form>
<script
type=
"text/javascript"
>
(
function
(
d
)
{
var
element
=
d
.
getElementById
(
"lti-${element_id}"
);
if
(
element
)
{
element
.
submit
();
}
}(
document
));
</script>
</body>
</html>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment