From c8964b800bcb4a85402d5900728035dc62cdd807 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 15:14:18 -0700 Subject: [PATCH 001/143] Create jsloader.coffee --- common/lib/xmodule/xmodule/html_module.py | 5 ++++- common/lib/xmodule/xmodule/js/src/capa/display.coffee | 1 + common/lib/xmodule/xmodule/js/src/html/display.coffee | 7 ++++--- common/lib/xmodule/xmodule/js/src/jsloader.coffee | 3 +++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/jsloader.coffee diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 216b63a12e..c41bbbd413 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -17,7 +17,10 @@ log = logging.getLogger("mitx.courseware") class HtmlModule(XModule): - js = {'coffee': [resource_string(__name__, 'js/src/html/display.coffee')]} + js = {'coffee': [resource_string(__name__, 'js/src/jsloader.coffee'), + resource_string(__name__, 'js/src/html/display.coffee') + ] + } js_module_name = "HTMLModule" def get_html(self): diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 3b45723ae5..e3ece7447c 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -27,6 +27,7 @@ class @Problem @$('section.action input.save').click @save # Collapsibles + JavascriptLoader.setCollapsibles() @$('.longform').hide(); @$('.shortform').append('See full output'); @$('.collapsible section').hide(); diff --git a/common/lib/xmodule/xmodule/js/src/html/display.coffee b/common/lib/xmodule/xmodule/js/src/html/display.coffee index 05e2ddab28..a3523e8332 100644 --- a/common/lib/xmodule/xmodule/js/src/html/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/display.coffee @@ -1,8 +1,9 @@ class @HTMLModule constructor: (@element) -> - @el = $(@element) - @setCollapsibles() + @el = $(@element) + JavascriptLoader.setCollapsibles() + @setCollapsibles() $: (selector) -> $(selector, @el) @@ -23,4 +24,4 @@ class @HTMLModule toggleHint: (event) => event.preventDefault() $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') \ No newline at end of file + $(event.target).parent().parent().toggleClass('open') diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee new file mode 100644 index 0000000000..3883d4a5a7 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -0,0 +1,3 @@ +class @JavascriptLoader + @setCollapsibles: -> + console.log('setCollapsibles!') From cbd602b40d9d6cf80cda03a498247728b2570f11 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 16:03:17 -0700 Subject: [PATCH 002/143] Moving toggle code. wip --- .../xmodule/js/src/capa/display.coffee | 16 ------------ .../xmodule/xmodule/js/src/jsloader.coffee | 26 +++++++++++++++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index e3ece7447c..1ed031b3b0 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -28,11 +28,6 @@ class @Problem # Collapsibles JavascriptLoader.setCollapsibles() - @$('.longform').hide(); - @$('.shortform').append('See full output'); - @$('.collapsible section').hide(); - @$('.full').click @toggleFull - @$('.collapsible header a').click @toggleHint # Dynamath @$('input.math').keyup(@refreshMath) @@ -341,17 +336,6 @@ class @Problem element.CodeMirror.save() if element.CodeMirror.save @answers = @inputs.serialize() - toggleFull: (event) => - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') - text = $(event.target).text() == 'See full output' ? 'Hide output' : 'See full output' - $(this).text(text) - - toggleHint: (event) => - event.preventDefault() - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') - inputtypeSetupMethods: 'text-input-dynamath': (element) => diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 3883d4a5a7..345bcde75f 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -1,3 +1,25 @@ class @JavascriptLoader - @setCollapsibles: -> - console.log('setCollapsibles!') + ### + Set of library functions that provide common interface for javascript loading + for all module types + ### + @setCollapsibles: () => + console.log($('.collapsible section')) + $('.longform').hide(); + $('.shortform').append('See full output'); + $('.collapsible section').hide(); + $('.full').click @toggleFull + $('.collapsible header a').click @toggleHint + @toggleHint() + + @toggleFull: (event) => + $(event.target).parent().siblings().slideToggle() + $(event.target).parent().parent().toggleClass('open') + text = $(event.target).text() == 'See full output' ? 'Hide output' : 'See full output' + $(this).text(text) + + @toggleHint: (event) => + console.log('toggleHint') + event.preventDefault() + $(event.target).parent().siblings().slideToggle() + $(event.target).parent().parent().toggleClass('open') From 65b3f58338e172a17de1b933c9476664b41acf6a Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 16:57:23 -0700 Subject: [PATCH 003/143] jsloader works at module-level --- .../xmodule/xmodule/js/src/capa/display.coffee | 2 +- .../lib/xmodule/xmodule/js/src/jsloader.coffee | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 1ed031b3b0..9b6d198359 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -27,7 +27,7 @@ class @Problem @$('section.action input.save').click @save # Collapsibles - JavascriptLoader.setCollapsibles() + JavascriptLoader.setCollapsibles(@el) # Dynamath @$('input.math').keyup(@refreshMath) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 345bcde75f..a83584dfa7 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -3,14 +3,15 @@ class @JavascriptLoader Set of library functions that provide common interface for javascript loading for all module types ### - @setCollapsibles: () => - console.log($('.collapsible section')) - $('.longform').hide(); - $('.shortform').append('See full output'); - $('.collapsible section').hide(); - $('.full').click @toggleFull - $('.collapsible header a').click @toggleHint - @toggleHint() + @setCollapsibles: (el) => + ### + el: jQuery object representing xmodule + ### + el.find('.longform').hide(); + el.find('.shortform').append('See full output'); + el.find('.collapsible section').hide(); + el.find('.full').click @toggleFull + el.find('.collapsible header a').click @toggleHint @toggleFull: (event) => $(event.target).parent().siblings().slideToggle() @@ -19,7 +20,6 @@ class @JavascriptLoader $(this).text(text) @toggleHint: (event) => - console.log('toggleHint') event.preventDefault() $(event.target).parent().siblings().slideToggle() $(event.target).parent().parent().toggleClass('open') From 46598b659c9ceba42362638faf14e485ec2a3312 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 17:27:06 -0700 Subject: [PATCH 004/143] HTML module also uses jsloader --- .../xmodule/js/src/html/display.coffee | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/html/display.coffee b/common/lib/xmodule/xmodule/js/src/html/display.coffee index a3523e8332..5e2ab94031 100644 --- a/common/lib/xmodule/xmodule/js/src/html/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/display.coffee @@ -2,26 +2,7 @@ class @HTMLModule constructor: (@element) -> @el = $(@element) - JavascriptLoader.setCollapsibles() - @setCollapsibles() + JavascriptLoader.setCollapsibles(@el) $: (selector) -> $(selector, @el) - - setCollapsibles: => - $('.longform').hide(); - $('.shortform').append('See full output'); - $('.collapsible section').hide(); - $('.full').click @toggleFull - $('.collapsible header a').click @toggleHint - - toggleFull: (event) => - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') - text = $(event.target).text() == 'See full output' ? 'Hide output' : 'See full output' - $(this).text(text) - - toggleHint: (event) => - event.preventDefault() - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') From 8956e5bcb193dcd7a0ac57b6db6f3c7d478542be Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 18:03:59 -0700 Subject: [PATCH 005/143] Update comment: jsloader works at module-scope --- common/lib/xmodule/xmodule/js/src/jsloader.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index a83584dfa7..8872228192 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -1,7 +1,8 @@ class @JavascriptLoader ### Set of library functions that provide common interface for javascript loading - for all module types + for all module types. All functionality provided by JavascriptLoader should take + place at module scope, i.e. don't run jQuery over entire page ### @setCollapsibles: (el) => ### From 77af860445fc69106fea32240869b540f69f9f89 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 18:31:06 -0700 Subject: [PATCH 006/143] Begin transplant of executeProblemScripts --- .../xmodule/js/src/capa/display.coffee | 8 +++++++ .../xmodule/xmodule/js/src/jsloader.coffee | 22 ++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 9b6d198359..95820838f5 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -77,6 +77,10 @@ class @Problem render: (content) -> if content @el.html(content) + JavascriptLoader.executeModuleScripts @el, () => + @setupInputTypes() + @bind() + @queueing() @executeProblemScripts () => @setupInputTypes() @bind() @@ -84,6 +88,10 @@ class @Problem else $.postWithPrefix "#{@url}/problem_get", (response) => @el.html(response.html) + JavascriptLoader.executeModuleScripts @el, () => + @setupInputTypes() + @bind() + @queueing() @executeProblemScripts () => @setupInputTypes() @bind() diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 8872228192..59c051d323 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -1,9 +1,21 @@ class @JavascriptLoader - ### - Set of library functions that provide common interface for javascript loading - for all module types. All functionality provided by JavascriptLoader should take - place at module scope, i.e. don't run jQuery over entire page - ### + + # Set of library functions that provide common interface for javascript loading + # for all module types. All functionality provided by JavascriptLoader should take + # place at module scope, i.e. don't run jQuery over entire page + + # executeModuleScripts: + # Scan module contents for "script_placeholder"s, then: + # 1) Fetch each script from server + # 2) Explicitly attach the script to the of document + # 3) Explicitly wait for each script to be loaded + # 4) Return to callback function when all scripts loaded + @executeModuleScripts: (el, callback=null) -> + console.log('executeModuleScripts') + + + # setCollapsibles: + # Scan module contents for generic collapsible containers @setCollapsibles: (el) => ### el: jQuery object representing xmodule From faa35f95cc20d1690899bdd4439c82f8d3039234 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 18:53:24 -0700 Subject: [PATCH 007/143] Move executeProblemScripts into executeModuleScripts --- .../xmodule/js/src/capa/display.coffee | 55 +------------------ .../xmodule/xmodule/js/src/jsloader.coffee | 46 +++++++++++++++- 2 files changed, 47 insertions(+), 54 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 95820838f5..8c726f02e7 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -63,7 +63,7 @@ class @Problem @new_queued_items = $(response.html).find(".xqueue") if @new_queued_items.length isnt @num_queued_items @el.html(response.html) - @executeProblemScripts () => + JavascriptLoader.executeModuleScripts @el, () => @setupInputTypes() @bind() @@ -81,10 +81,6 @@ class @Problem @setupInputTypes() @bind() @queueing() - @executeProblemScripts () => - @setupInputTypes() - @bind() - @queueing() else $.postWithPrefix "#{@url}/problem_get", (response) => @el.html(response.html) @@ -92,10 +88,7 @@ class @Problem @setupInputTypes() @bind() @queueing() - @executeProblemScripts () => - @setupInputTypes() - @bind() - @queueing() + # TODO add hooks for problem types here by inspecting response.html and doing # stuff if a div w a class is found @@ -110,50 +103,6 @@ class @Problem if setupMethod? @inputtypeDisplays[id] = setupMethod(inputtype) - executeProblemScripts: (callback=null) -> - - placeholders = @el.find(".script_placeholder") - - if placeholders.length == 0 - callback() - return - - completed = (false for i in [1..placeholders.length]) - callbackCalled = false - - # This is required for IE8 support. - completionHandlerGeneratorIE = (index) => - return () -> - if (this.readyState == 'complete' || this.readyState == 'loaded') - #completionHandlerGenerator.call(self, index)() - completionHandlerGenerator(index)() - - completionHandlerGenerator = (index) => - return () => - allComplete = true - completed[index] = true - for flag in completed - if not flag - allComplete = false - break - if allComplete and not callbackCalled - callbackCalled = true - callback() if callback? - - placeholders.each (index, placeholder) -> - s = document.createElement('script') - s.setAttribute('src', $(placeholder).attr("data-src")) - s.setAttribute('type', "text/javascript") - - s.onload = completionHandlerGenerator(index) - - # s.onload does not fire in IE8; this does. - s.onreadystatechange = completionHandlerGeneratorIE(index) - - # Need to use the DOM elements directly or the scripts won't execute - # properly. - $('head')[0].appendChild(s) - $(placeholder).remove() ### # 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 59c051d323..254c1b84db 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -11,7 +11,51 @@ class @JavascriptLoader # 3) Explicitly wait for each script to be loaded # 4) Return to callback function when all scripts loaded @executeModuleScripts: (el, callback=null) -> - console.log('executeModuleScripts') + + placeholders = el.find(".script_placeholder") + + if placeholders.length == 0 + callback() + return + + completed = (false for i in [1..placeholders.length]) + callbackCalled = false + + # This is required for IE8 support. + completionHandlerGeneratorIE = (index) => + return () -> + if (this.readyState == 'complete' || this.readyState == 'loaded') + #completionHandlerGenerator.call(self, index)() + completionHandlerGenerator(index)() + + completionHandlerGenerator = (index) => + return () => + allComplete = true + completed[index] = true + for flag in completed + if not flag + allComplete = false + break + if allComplete and not callbackCalled + callbackCalled = true + callback() if callback? + + placeholders.each (index, placeholder) -> + # TODO: Check if the script already exists in DOM. If so, (1) copy it + # into memory; (2) delete the DOM script element; (3) reappend it + s = document.createElement('script') + s.setAttribute('src', $(placeholder).attr("data-src")) + s.setAttribute('type', "text/javascript") + + s.onload = completionHandlerGenerator(index) + + # s.onload does not fire in IE8; this does. + s.onreadystatechange = completionHandlerGeneratorIE(index) + + # Need to use the DOM elements directly or the scripts won't execute + # properly. + $('head')[0].appendChild(s) + $(placeholder).remove() # setCollapsibles: From 4acd8b9fe1c46561f79ad0dba7639e699d141c5d Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 18:54:17 -0700 Subject: [PATCH 008/143] Further explanation to TODO --- common/lib/xmodule/xmodule/js/src/jsloader.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 254c1b84db..18455da050 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -42,7 +42,8 @@ class @JavascriptLoader placeholders.each (index, placeholder) -> # TODO: Check if the script already exists in DOM. If so, (1) copy it - # into memory; (2) delete the DOM script element; (3) reappend it + # into memory; (2) delete the DOM script element; (3) reappend it. + # This would prevent memory bloat and save a network request. s = document.createElement('script') s.setAttribute('src', $(placeholder).attr("data-src")) s.setAttribute('type', "text/javascript") From 7b8750ab69bd02d8d60db379f70c63552ce3f5bf Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 19:05:48 -0700 Subject: [PATCH 009/143] HTML modules can also run script_placeholder --- common/lib/xmodule/xmodule/js/src/html/display.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/js/src/html/display.coffee b/common/lib/xmodule/xmodule/js/src/html/display.coffee index 5e2ab94031..a3e55f8f6c 100644 --- a/common/lib/xmodule/xmodule/js/src/html/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/display.coffee @@ -2,6 +2,7 @@ class @HTMLModule constructor: (@element) -> @el = $(@element) + JavascriptLoader.executeModuleScripts(@el) JavascriptLoader.setCollapsibles(@el) $: (selector) -> From c3abeff8d4e700ec2d064c214e956efc631335a9 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 21:28:04 -0700 Subject: [PATCH 010/143] Another TODO comment --- common/lib/xmodule/xmodule/js/src/jsloader.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 18455da050..441bf42975 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -18,6 +18,7 @@ class @JavascriptLoader callback() return + # TODO: Verify the execution order of multiple placeholders completed = (false for i in [1..placeholders.length]) callbackCalled = false From 680934045fac7d8ab1a59dd902abf997811d2786 Mon Sep 17 00:00:00 2001 From: kimth Date: Sat, 29 Sep 2012 21:39:48 -0700 Subject: [PATCH 011/143] Adjust comments --- common/lib/xmodule/xmodule/js/src/jsloader.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/jsloader.coffee index 441bf42975..8cc524520a 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/jsloader.coffee @@ -5,7 +5,7 @@ class @JavascriptLoader # place at module scope, i.e. don't run jQuery over entire page # executeModuleScripts: - # Scan module contents for "script_placeholder"s, then: + # Scan the module ('el') for "script_placeholder"s, then: # 1) Fetch each script from server # 2) Explicitly attach the script to the of document # 3) Explicitly wait for each script to be loaded From a8acb7ff30481bdd9c29237c491773cb90100386 Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Mon, 1 Oct 2012 03:03:33 -0700 Subject: [PATCH 012/143] Renamed jsloader to match up with the content more explicitly; refactored Collapsible out into its own library --- common/lib/xmodule/xmodule/capa_module.py | 5 +++- common/lib/xmodule/xmodule/html_module.py | 2 +- .../xmodule/js/src/capa/display.coffee | 2 +- .../xmodule/js/src/html/display.coffee | 2 +- ...loader.coffee => javascript_loader.coffee} | 24 ----------------- lms/static/coffee/src/collapsible.coffee | 27 +++++++++++++++++++ 6 files changed, 34 insertions(+), 28 deletions(-) rename common/lib/xmodule/xmodule/js/src/{jsloader.coffee => javascript_loader.coffee} (72%) create mode 100644 lms/static/coffee/src/collapsible.coffee diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 4211434619..fc71d2da02 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -75,9 +75,12 @@ class CapaModule(XModule): ''' icon_class = 'problem' - js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee')], + js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), + resource_string(__name__, 'js/src/javascript_loader.coffee'), + ], 'js': [resource_string(__name__, 'js/src/capa/imageinput.js'), resource_string(__name__, 'js/src/capa/schematic.js')]} + js_module_name = "Problem" css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index c41bbbd413..d51c8ad4a7 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -17,7 +17,7 @@ log = logging.getLogger("mitx.courseware") class HtmlModule(XModule): - js = {'coffee': [resource_string(__name__, 'js/src/jsloader.coffee'), + js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/html/display.coffee') ] } diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 8c726f02e7..123f68145a 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -27,7 +27,7 @@ class @Problem @$('section.action input.save').click @save # Collapsibles - JavascriptLoader.setCollapsibles(@el) + Collapsible.setCollapsibles(@el) # Dynamath @$('input.math').keyup(@refreshMath) diff --git a/common/lib/xmodule/xmodule/js/src/html/display.coffee b/common/lib/xmodule/xmodule/js/src/html/display.coffee index a3e55f8f6c..d39bacc8d0 100644 --- a/common/lib/xmodule/xmodule/js/src/html/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/html/display.coffee @@ -3,7 +3,7 @@ class @HTMLModule constructor: (@element) -> @el = $(@element) JavascriptLoader.executeModuleScripts(@el) - JavascriptLoader.setCollapsibles(@el) + Collapsible.setCollapsibles(@el) $: (selector) -> $(selector, @el) diff --git a/common/lib/xmodule/xmodule/js/src/jsloader.coffee b/common/lib/xmodule/xmodule/js/src/javascript_loader.coffee similarity index 72% rename from common/lib/xmodule/xmodule/js/src/jsloader.coffee rename to common/lib/xmodule/xmodule/js/src/javascript_loader.coffee index 8cc524520a..543aec8edc 100644 --- a/common/lib/xmodule/xmodule/js/src/jsloader.coffee +++ b/common/lib/xmodule/xmodule/js/src/javascript_loader.coffee @@ -58,27 +58,3 @@ class @JavascriptLoader # properly. $('head')[0].appendChild(s) $(placeholder).remove() - - - # setCollapsibles: - # Scan module contents for generic collapsible containers - @setCollapsibles: (el) => - ### - el: jQuery object representing xmodule - ### - el.find('.longform').hide(); - el.find('.shortform').append('See full output'); - el.find('.collapsible section').hide(); - el.find('.full').click @toggleFull - el.find('.collapsible header a').click @toggleHint - - @toggleFull: (event) => - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') - text = $(event.target).text() == 'See full output' ? 'Hide output' : 'See full output' - $(this).text(text) - - @toggleHint: (event) => - event.preventDefault() - $(event.target).parent().siblings().slideToggle() - $(event.target).parent().parent().toggleClass('open') diff --git a/lms/static/coffee/src/collapsible.coffee b/lms/static/coffee/src/collapsible.coffee new file mode 100644 index 0000000000..6e469d3704 --- /dev/null +++ b/lms/static/coffee/src/collapsible.coffee @@ -0,0 +1,27 @@ +class @Collapsible + + # Set of library functions that provide a simple way to add collapsible + # functionality to elements. + + # setCollapsibles: + # Scan element's content for generic collapsible containers + @setCollapsibles: (el) => + ### + el: container + ### + el.find('.longform').hide() + el.find('.shortform').append('See full output') + el.find('.collapsible section').hide() + el.find('.full').click @toggleFull + el.find('.collapsible header a').click @toggleHint + + @toggleFull: (event) => + $(event.target).parent().siblings().slideToggle() + $(event.target).parent().parent().toggleClass('open') + text = $(event.target).text() == 'See full output' ? 'Hide output' : 'See full output' + $(this).text(text) + + @toggleHint: (event) => + event.preventDefault() + $(event.target).parent().siblings().slideToggle() + $(event.target).parent().parent().toggleClass('open') From 258a21055626f7d8104960f14d74919a3abf7fac Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 2 Oct 2012 11:04:43 -0400 Subject: [PATCH 013/143] Don't ignore .py files in static files dirs --- lms/envs/common.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index d9f8a873d1..d71f654d67 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -585,8 +585,6 @@ PIPELINE_JS_COMPRESSOR = None STATICFILES_IGNORE_PATTERNS = ( "sass/*", "coffee/*", - "*.py", - "*.pyc" ) PIPELINE_YUI_BINARY = 'yui-compressor' From ed55e4ae2fc2564a576dd6e42a6840721b33995b Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 2 Oct 2012 15:41:38 +0000 Subject: [PATCH 014/143] Move collapsible back into xmodule --- common/lib/xmodule/xmodule/capa_module.py | 1 + common/lib/xmodule/xmodule/html_module.py | 1 + .../lib/xmodule/xmodule/js}/src/collapsible.coffee | 0 3 files changed, 2 insertions(+) rename {lms/static/coffee => common/lib/xmodule/xmodule/js}/src/collapsible.coffee (100%) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index fc71d2da02..15e2af86eb 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -76,6 +76,7 @@ class CapaModule(XModule): icon_class = 'problem' js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/javascript_loader.coffee'), ], 'js': [resource_string(__name__, 'js/src/capa/imageinput.js'), diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index d51c8ad4a7..ffd80d238b 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -18,6 +18,7 @@ log = logging.getLogger("mitx.courseware") class HtmlModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), + resource_string(__name__, 'js/src/collapsible.coffee') resource_string(__name__, 'js/src/html/display.coffee') ] } diff --git a/lms/static/coffee/src/collapsible.coffee b/common/lib/xmodule/xmodule/js/src/collapsible.coffee similarity index 100% rename from lms/static/coffee/src/collapsible.coffee rename to common/lib/xmodule/xmodule/js/src/collapsible.coffee From 845b921ca12cb8b58b196c45bc2c7bebf87c351a Mon Sep 17 00:00:00 2001 From: kimth Date: Tue, 2 Oct 2012 15:42:22 +0000 Subject: [PATCH 015/143] Missing comma --- common/lib/xmodule/xmodule/html_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index ffd80d238b..20fda1e9f5 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -18,7 +18,7 @@ log = logging.getLogger("mitx.courseware") class HtmlModule(XModule): js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), - resource_string(__name__, 'js/src/collapsible.coffee') + resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/html/display.coffee') ] } From 8ad1e7fba18320505ed2f06686bdf4c1f1d57303 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 2 Oct 2012 11:48:16 -0400 Subject: [PATCH 016/143] Add framework for a site status banner. - still needs style - will cache for 10 seconds --- common/djangoapps/status/__init__.py | 1 + common/djangoapps/status/status.py | 43 ++++++++++++++++++++++++++++ lms/envs/common.py | 26 +++++++++++------ lms/templates/navigation.html | 22 +++++++++----- 4 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 common/djangoapps/status/__init__.py create mode 100644 common/djangoapps/status/status.py diff --git a/common/djangoapps/status/__init__.py b/common/djangoapps/status/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/djangoapps/status/__init__.py @@ -0,0 +1 @@ + diff --git a/common/djangoapps/status/status.py b/common/djangoapps/status/status.py new file mode 100644 index 0000000000..ab1624aed2 --- /dev/null +++ b/common/djangoapps/status/status.py @@ -0,0 +1,43 @@ +""" +A tiny app that checks for a status message. +""" + +from django.conf import settings +import logging +import os +import sys + +from util.cache import cache + +log = logging.getLogger(__name__) + +def get_site_status_msg(): + """ + Look for a file settings.STATUS_MESSAGE_PATH. If found, return the + contents. Otherwise, return None. Caches result for 10 seconds, per-machine. + + If something goes wrong, returns None. ("is there a status msg?" logic is + not allowed to break the entire site). + """ + cache_time = 10 + try: + key = ','.join([settings.HOSTNAME, settings.STATUS_MESSAGE_PATH]) + content = cache.get(key) + if content == '': + # cached that there isn't a status message + return None + + if content is None: + # nothing in the cache, so check the filesystem + if os.path.isfile(settings.STATUS_MESSAGE_PATH): + with open(settings.STATUS_MESSAGE_PATH) as f: + content = f.read() + else: + # remember that there isn't anything there + cache.set(key, '', cache_time) + content = None + + return content + except: + log.debug("Error while getting a status message: {0}".format(sys.exc_info())) + return None diff --git a/lms/envs/common.py b/lms/envs/common.py index d9f8a873d1..bbd36c1ae8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -25,6 +25,7 @@ import glob2 import errno import hashlib from collections import defaultdict +import socket import djcelery from path import path @@ -95,6 +96,13 @@ GENERATE_PROFILE_SCORES = False # Used with XQueue XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds + +# Used for per-maching caching +try: + HOSTNAME = socket.gethostname() +except: + HOSTNAME = 'localhost' + ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms REPO_ROOT = PROJECT_ROOT.dirname() @@ -103,7 +111,6 @@ ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in ASKBOT_ROOT = REPO_ROOT / "askbot" COURSES_ROOT = ENV_ROOT / "data" -# FIXME: To support multiple courses, we should walk the courses dir at startup DATA_DIR = COURSES_ROOT sys.path.append(REPO_ROOT) @@ -127,8 +134,11 @@ node_paths = [COMMON_ROOT / "static/js/vendor", NODE_PATH = ':'.join(node_paths) +# Where to look for a status message +STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.html" + ############################ OpenID Provider ################################## -OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net'] +OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net'] ################################## MITXWEB ##################################### # This is where we stick our compiled template files. Most of the app uses Mako @@ -158,7 +168,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'askbot.user_messages.context_processors.user_messages',#must be before auth 'django.contrib.auth.context_processors.auth', #this is required for admin 'django.core.context_processors.csrf', #necessary for csrf protection - + # Added for django-wiki 'django.core.context_processors.media', 'django.core.context_processors.tz', @@ -355,7 +365,7 @@ WIKI_CAN_ASSIGN = lambda article, user: user.is_staff or user.is_superuser WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False WIKI_LINK_LIVE_LOOKUPS = False -WIKI_LINK_DEFAULT_LEVEL = 2 +WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' @@ -372,10 +382,10 @@ STATICFILES_FINDERS = ( TEMPLATE_LOADERS = ( 'mitxmako.makoloader.MakoFilesystemLoader', 'mitxmako.makoloader.MakoAppDirectoriesLoader', - + # 'django.template.loaders.filesystem.Loader', # 'django.template.loaders.app_directories.Loader', - + #'askbot.skins.loaders.filesystem_load_template_source', # 'django.template.loaders.eggs.Loader', ) @@ -393,7 +403,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'track.middleware.TrackMiddleware', 'mitxmako.middleware.MakoMiddleware', - + 'course_wiki.course_nav.Middleware', 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', @@ -624,7 +634,7 @@ INSTALLED_APPS = ( 'certificates', 'instructor', 'psychometrics', - + #For the wiki 'wiki', # The new django-wiki from benjaoming 'django_notify', diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 210fd61ead..b1b2ddad60 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -8,19 +8,27 @@ from django.core.urlresolvers import reverse # App that handles subdomain specific branding import branding +# app that handles site status messages +from status.status import get_site_status_msg +site_status_msg = get_site_status_msg() %> -%if course: +% if site_status_msg: +
${site_status_msg}
+% endif + + +% if course:
-%else: +% else:
-%endif +% endif %endif +
+
Warning: Your browser is not fully supported. We strongly recommend using Chrome or Firefox.
%if not user.is_authenticated(): <%include file="login_modal.html" /> From 1804d5410c8b2d02d161cee3993ca905bc6b7a87 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 4 Oct 2012 15:50:02 -0400 Subject: [PATCH 035/143] fixed ie banner showing outside of courseware bug --- lms/static/sass/base/_base.scss | 17 +++++++++++++++++ .../sass/course/layout/_courseware_header.scss | 17 ----------------- lms/templates/navigation.html | 2 ++ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss index aeaaf146d9..ca56f542d6 100644 --- a/lms/static/sass/base/_base.scss +++ b/lms/static/sass/base/_base.scss @@ -185,5 +185,22 @@ mark { } } +.ie-banner { + display: none; + max-width: 1140px; + min-width: 720px; + margin: auto; + @include border-radius(0 0 3px 3px); + background: #f4f4e0; + color: #3c3c3c; + padding: 5px 20px 8px; + font-size: 13px; + text-align: center; + + strong { + font-weight: 700; + } +} + diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index b83b28301c..b5c93f8e14 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -168,21 +168,4 @@ header.global.slim { font-weight: bold; letter-spacing: 0; } -} - -.ie-banner { - display: none; - max-width: 1140px; - min-width: 720px; - margin: auto; - @include border-radius(0 0 3px 3px); - background: #f4f4e0; - color: #3c3c3c; - padding: 5px 20px 8px; - font-size: 13px; - text-align: center; - - strong { - font-weight: 700; - } } \ No newline at end of file diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 2c4b8863d6..90c9e2d383 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -82,7 +82,9 @@ site_status_msg = get_site_status_msg() %endif
+% if course:
Warning: Your browser is not fully supported. We strongly recommend using Chrome or Firefox.
+% endif %if not user.is_authenticated(): <%include file="login_modal.html" /> From 4fd530dbac038516dee41226935753c7e9b7d436 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 5 Oct 2012 11:49:17 -0400 Subject: [PATCH 036/143] fix python syntax in template for adding google analytics --- lms/templates/main.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/main.html b/lms/templates/main.html index 861059da5d..f234aa72cf 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -20,7 +20,7 @@ - % if course == false: + % if not course: ''' - snippets = [{'snippet': ''' + snippets = [{'snippet': """
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\) @@ -802,8 +839,8 @@ class CustomResponse(LoncapaResponse): if not(r=="IS*u(t-t0)"): correct[0] ='incorrect' -
'''}, - {'snippet': ''' stanza instead + # if we have a "cfn" attribute then look for the function specified by cfn, in + # the problem context ie the comparison function is defined in the + # stanza instead cfn = xml.get('cfn') if cfn: log.debug("cfn = %s" % cfn) @@ -847,13 +886,14 @@ def sympy_check2(): self.code = self.context[cfn] else: msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) - msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', + '') raise LoncapaProblemError(msg) if not self.code: if answer is None: - # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid - log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) + log.error("[courseware.capa.responsetypes.customresponse] missing" + " code checking script! id=%s" % self.myid) self.code = '' else: answer_src = answer.get('src') @@ -870,43 +910,70 @@ def sympy_check2(): log.debug('%s: student_answers=%s' % (unicode(self), student_answers)) - idset = sorted(self.answer_ids) # ordered list of answer id's + # ordered list of answer id's + idset = sorted(self.answer_ids) try: - submission = [student_answers[k] for k in idset] # ordered list of answers + # ordered list of answers + submission = [student_answers[k] for k in idset] except Exception as err: - msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers + msg = ('[courseware.capa.responsetypes.customresponse] error getting' + ' student answer from %s' % student_answers) msg += '\n idset = %s, error = %s' % (idset, err) log.error(msg) raise Exception(msg) # global variable in context which holds the Presentation MathML from dynamic math input - dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses + # ordered list of dynamath responses + dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # if there is only one box, and it's empty, then don't evaluate if len(idset) == 1 and not submission[0]: - # default to no error message on empty answer (to be consistent with other responsetypes) - # but allow author to still have the old behavior by setting empty_answer_err attribute - msg = 'No answer entered!' if self.xml.get('empty_answer_err') else '' + # default to no error message on empty answer (to be consistent with other + # responsetypes) but allow author to still have the old behavior by setting + # empty_answer_err attribute + msg = ('No answer entered!' + if self.xml.get('empty_answer_err') else '') return CorrectMap(idset[0], 'incorrect', msg=msg) - # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's + # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are + # not expecting 'unknown's correct = ['unknown'] * len(idset) messages = [''] * len(idset) # put these in the context of the check function evaluator # note that this doesn't help the "cfn" version - only the exec version - self.context.update({'xml': self.xml, # our subtree - 'response_id': self.myid, # my ID - 'expect': self.expect, # expected answer (if given as attribute) - 'submission': submission, # ordered list of student answers from entry boxes in our subtree - 'idset': idset, # ordered list of ID's of all entry boxes in our subtree - 'dynamath': dynamath, # ordered list of all javascript inputs in our subtree - 'answers': student_answers, # dict of student's responses, with keys being entry box IDs - 'correct': correct, # the list to be filled in by the check function - 'messages': messages, # the list of messages to be filled in by the check function - 'options': self.xml.get('options'), # any options to be passed to the cfn - 'testdat': 'hello world', - }) + self.context.update({ + # our subtree + 'xml': self.xml, + + # my ID + 'response_id': self.myid, + + # expected answer (if given as attribute) + 'expect': self.expect, + + # ordered list of student answers from entry boxes in our subtree + 'submission': submission, + + # ordered list of ID's of all entry boxes in our subtree + 'idset': idset, + + # ordered list of all javascript inputs in our subtree + 'dynamath': dynamath, + + # dict of student's responses, with keys being entry box IDs + 'answers': student_answers, + + # the list to be filled in by the check function + 'correct': correct, + + # the list of messages to be filled in by the check function + 'messages': messages, + + # any options to be passed to the cfn + 'options': self.xml.get('options'), + 'testdat': 'hello world', + }) # pass self.system.debug to cfn self.context['debug'] = self.system.DEBUG @@ -921,8 +988,10 @@ def sympy_check2(): print "oops in customresponse (code) error %s" % err print "context = ", self.context print traceback.format_exc() - raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student - else: # self.code is not a string; assume its a function + # Notify student + raise StudentInputError("Error: Problem could not be evaluated with your input") + else: + # self.code is not a string; assume its a function # this is an interface to the Tutor2 check functions fn = self.code @@ -958,7 +1027,8 @@ def sympy_check2(): msg = '' + msg + '' msg = msg.replace('<', '<') #msg = msg.replace('<','<') - msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), + pretty_print=True) #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) msg = msg.replace(' ', '') #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 @@ -1022,18 +1092,19 @@ class SymbolicResponse(CustomResponse): class CodeResponse(LoncapaResponse): - ''' + """ Grade student code using an external queueing server, called 'xqueue' Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse: system.xqueue = { 'interface': XqueueInterface object, - 'callback_url': Per-StudentModule callback URL where results are posted (string), + 'callback_url': Per-StudentModule callback URL + where results are posted (string), 'default_queuename': Default queuename to submit request (string) } - External requests are only submitted for student submission grading + External requests are only submitted for student submission grading (i.e. and not for getting reference answers) - ''' + """ response_tag = 'coderesponse' allowed_inputfields = ['textbox', 'filesubmission'] @@ -1046,7 +1117,8 @@ class CodeResponse(LoncapaResponse): TODO: Determines whether in synchronous or asynchronous (queued) mode ''' xml = self.xml - self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL + # TODO: XML can override external resource (grader/queue) URL + self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) # VS[compat]: @@ -1107,7 +1179,8 @@ class CodeResponse(LoncapaResponse): # Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is: # (1) Internal edX code, i.e. NOT student submissions, and - # (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program' + # (2) The code should only define the strings 'initial_display', 'answer', + # 'preamble', 'test_program' # following the ExternalResponse XML format penv = {} penv['__builtins__'] = globals()['__builtins__'] @@ -1120,10 +1193,12 @@ class CodeResponse(LoncapaResponse): self.answer = penv['answer'] self.initial_display = penv['initial_display'] except Exception as err: - log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in ..." % err) + log.error("Error in CodeResponse %s: Problem reference code does not define" + " 'answer' and/or 'initial_display' in ..." % err) raise Exception(err) - # Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface + # Finally, make the ExternalResponse input XML format conform to the generic + # exteral grader interface # The XML tagging of grader_payload is pyxserver-specific grader_payload = '' grader_payload += '' + tests + '\n' @@ -1133,14 +1208,16 @@ class CodeResponse(LoncapaResponse): def get_score(self, student_answers): try: - submission = student_answers[self.answer_id] # Note that submission can be a file + # Note that submission can be a file + submission = student_answers[self.answer_id] except Exception as err: - log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' % + log.error('Error in CodeResponse %s: cannot get student answer for %s;' + ' student_answers=%s' % (err, self.answer_id, convert_files_to_filenames(student_answers))) raise Exception(err) # Prepare xqueue request - #------------------------------------------------------------ + #------------------------------------------------------------ qinterface = self.system.xqueue['interface'] qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) @@ -1149,19 +1226,20 @@ class CodeResponse(LoncapaResponse): # Generate header queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + - anonymous_student_id + + anonymous_student_id + self.answer_id) xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) - + # Generate body if is_list_of_files(submission): - self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue + # TODO: Get S3 pointer from the Queue + self.context.update({'submission': ''}) else: self.context.update({'submission': submission}) - contents = self.payload.copy() + contents = self.payload.copy() # Metadata related to the student submission revealed to the external grader student_info = {'anonymous_student_id': anonymous_student_id, @@ -1171,7 +1249,8 @@ class CodeResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue if is_list_of_files(submission): - contents.update({'student_response': ''}) # TODO: Is there any information we want to send here? + # TODO: Is there any information we want to send here? + contents.update({'student_response': ''}) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents), files_to_upload=submission) @@ -1182,44 +1261,51 @@ class CodeResponse(LoncapaResponse): # State associated with the queueing request queuestate = {'key': queuekey, - 'time': qtime, - } + 'time': qtime,} - cmap = CorrectMap() + cmap = CorrectMap() if error: cmap.set(self.answer_id, queuestate=None, - msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) + msg='Unable to deliver your submission to grader. (Reason: %s.)' + ' Please try again later.' % msg) else: # Queueing mechanism flags: - # 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued - # 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox - # and .filesubmission to inform the browser to poll the LMS + # 1) Backend: Non-null CorrectMap['queuestate'] indicates that + # the problem has been queued + # 2) Frontend: correctness='incomplete' eventually trickles down + # through inputtypes.textbox and .filesubmission to inform the + # browser to poll the LMS cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) return cmap def update_score(self, score_msg, oldcmap, queuekey): - (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) + (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: - oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.') + oldcmap.set(self.answer_id, + msg='Invalid grader reply. Please contact the course staff.') return oldcmap - + correctness = 'correct' if correct else 'incorrect' - self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any + # TODO: Find out how this is used elsewhere, if any + self.context['correct'] = correctness - # Replace 'oldcmap' with new grading results if queuekey matches. - # If queuekey does not match, we keep waiting for the score_msg whose key actually matches + # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # does not match, we keep waiting for the score_msg whose key actually matches if oldcmap.is_right_queuekey(self.answer_id, queuekey): - # Sanity check on returned points + # Sanity check on returned points if points < 0: points = 0 elif points > self.maxpoints[self.answer_id]: points = self.maxpoints[self.answer_id] - oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed + # Queuestate is consumed + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, + msg=msg.replace(' ', ' '), queuestate=None) else: - log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) + log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % + (queuekey, self.answer_id)) return oldcmap @@ -1231,7 +1317,7 @@ class CodeResponse(LoncapaResponse): return {self.answer_id: self.initial_display} def _parse_score_msg(self, score_msg): - ''' + """ Grader reply is a JSON-dump of the following dict { 'correct': True/False, 'score': Numeric value (floating point is okay) to assign to answer @@ -1242,22 +1328,25 @@ class CodeResponse(LoncapaResponse): correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) msg: Message from grader to display to student (string) - ''' + """ fail = (False, False, 0, '') try: score_result = json.loads(score_msg) except (TypeError, ValueError): - log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg) + log.error("External grader message should be a JSON-serialized dict." + " Received score_msg = %s" % score_msg) return fail if not isinstance(score_result, dict): - log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result) + log.error("External grader message should be a JSON-serialized dict." + " Received score_result = %s" % score_result) return fail for tag in ['correct', 'score', 'msg']: if tag not in score_result: - log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'") + log.error("External grader message is missing one or more required" + " tags: 'correct', 'score', 'msg'") return fail - # Next, we need to check that the contents of the external grader message + # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? @@ -1265,11 +1354,12 @@ class CodeResponse(LoncapaResponse): try: etree.fromstring(msg) except etree.XMLSyntaxError as err: - log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg) + log.error("Unable to parse external grader message as valid" + " XML: score_msg['msg']=%s" % msg) return fail - + return (True, score_result['correct'], score_result['score'], msg) - + #----------------------------------------------------------------------------- @@ -1325,9 +1415,9 @@ main() def setup_response(self): xml = self.xml - self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + # FIXME - hardcoded URL + self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" - # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer = xml.find('answer') if answer is not None: answer_src = answer.get('src') @@ -1335,7 +1425,8 @@ main() self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text - else: # no stanza; get code from @@ -48,10 +75,17 @@ @@ -121,13 +155,11 @@ - - + + + diff --git a/lms/urls.py b/lms/urls.py index 662e41235e..ac61b85248 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -19,7 +19,7 @@ urlpatterns = ('', # (specifically missing get parameters in certain cases) url(r'^debug_request$', 'util.views.debug_request'), - url(r'^change_email$', 'student.views.change_email_request'), + url(r'^change_email$', 'student.views.change_email_request', name="change_email"), url(r'^email_confirm/(?P[^/]*)$', 'student.views.confirm_email_change'), url(r'^change_name$', 'student.views.change_name_request'), url(r'^accept_name_change$', 'student.views.accept_name_change'), From 19d3cb3870bdce8dffced594bda0314daa316417 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 12 Oct 2012 13:52:48 -0400 Subject: [PATCH 086/143] Add a chemicalequationinput with live preview - architecturally slightly questionable: the preview ajax calls goes to an LMS view instead of an input type specific one. This needs to be fixed during the grand capa re-org, but there isn't time to do it right now. - also, I kind of like having a generic turn-a-formula-into-a-preview service available --- common/lib/capa/capa/capa_problem.py | 3 +- common/lib/capa/capa/chem/chemcalc.py | 94 +++++++++++++++---- common/lib/capa/capa/inputtypes.py | 31 ++++++ common/lib/capa/capa/responsetypes.py | 2 +- .../capa/templates/chemicalequationinput.html | 42 +++++++++ common/static/js/capa/README | 1 + .../js/capa/chemical_equation_preview.js | 12 +++ lms/djangoapps/courseware/module_render.py | 41 ++++++++ lms/urls.py | 10 ++ 9 files changed, 214 insertions(+), 22 deletions(-) create mode 100644 common/lib/capa/capa/templates/chemicalequationinput.html create mode 100644 common/static/js/capa/README create mode 100644 common/static/js/capa/chemical_equation_preview.js diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 252b536927..ca78f635e3 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -55,7 +55,8 @@ entry_types = ['textline', 'radiogroup', 'checkboxgroup', 'filesubmission', - 'javascriptinput',] + 'javascriptinput', + 'chemicalequationinput'] # extra things displayed after "show answers" is pressed solution_types = ['solution'] diff --git a/common/lib/capa/capa/chem/chemcalc.py b/common/lib/capa/capa/chem/chemcalc.py index 1df8302b37..79a788404d 100644 --- a/common/lib/capa/capa/chem/chemcalc.py +++ b/common/lib/capa/capa/chem/chemcalc.py @@ -125,8 +125,13 @@ def _merge_children(tree, tags): (group 1 2 3 4) It has to handle this recursively: (group 1 (group 2 (group 3 (group 4)))) - We do the cleanup of converting from the latter to the former (as a + We do the cleanup of converting from the latter to the former. ''' + if tree is None: + # There was a problem--shouldn't have empty trees (NOTE: see this with input e.g. 'H2O(', or 'Xe+'). + # Haven't grokked the code to tell if this is indeed the right thing to do. + raise ParseException("Shouldn't have empty trees") + if type(tree) == str: return tree @@ -195,14 +200,52 @@ def _render_to_html(tree): return children.replace(' ', '') -def render_to_html(s): - ''' render a string to html ''' - status = _render_to_html(_get_final_tree(s)) - return status + +def render_to_html(eq): + ''' + Render a chemical equation string to html. + + Renders each molecule separately, and returns invalid input wrapped in a . + ''' + def err(s): + "Render as an error span" + return '{0}'.format(s) + + def render_arrow(arrow): + """Turn text arrows into pretty ones""" + if arrow == '->': + return u'\u2192' + if arrow == '<->': + return u'\u2194' + return arrow + + def render_expression(ex): + """ + Render a chemical expression--no arrows. + """ + try: + return _render_to_html(_get_final_tree(ex)) + except ParseException: + return err(ex) + + def spanify(s): + return u'{0}'.format(s) + + left, arrow, right = split_on_arrow(eq) + if arrow == '': + # only one side + return spanify(render_expression(left)) + + + return spanify(render_expression(left) + render_arrow(arrow) + render_expression(right)) def _get_final_tree(s): - ''' return final tree after merge and clean ''' + ''' + Return final tree after merge and clean. + + Raises pyparsing.ParseException if s is invalid. + ''' tokenized = tokenizer.parseString(s) parsed = parser.parse(tokenized) merged = _merge_children(parsed, {'S','group'}) @@ -227,14 +270,14 @@ def _check_equality(tuple1, tuple2): def compare_chemical_expression(s1, s2, ignore_state=False): - ''' It does comparison between two equations. + ''' It does comparison between two expressions. It uses divide_chemical_expression and check if division is 1 ''' return divide_chemical_expression(s1, s2, ignore_state) == 1 def divide_chemical_expression(s1, s2, ignore_state=False): - '''Compare two chemical equations for equivalence up to a multiplicative factor: + '''Compare two chemical expressions for equivalence up to a multiplicative factor: - If they are not the same chemicals, returns False. - If they are the same, "divide" s1 by s2 to returns a factor x such that s1 / s2 == x as a Fraction object. @@ -248,7 +291,7 @@ def divide_chemical_expression(s1, s2, ignore_state=False): Implementation sketch: - extract factors and phases to standalone lists, - - compare equations without factors and phases, + - compare expressions without factors and phases, - divide lists of factors for each other and check for equality of every element in list, - return result of factor division @@ -294,7 +337,7 @@ def divide_chemical_expression(s1, s2, ignore_state=False): treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'] = zip( *sorted(zip(treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases']))) - # check if equations are correct without factors + # check if expressions are correct without factors if not _check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']): return False @@ -312,9 +355,26 @@ def divide_chemical_expression(s1, s2, ignore_state=False): return Fraction(treedic['1 factors'][0] / treedic['2 factors'][0]) +def split_on_arrow(eq): + """ + Split a string on an arrow. Returns left, arrow, right. If there is no arrow, returns the + entire eq in left, and '' in arrow and right. + + Return left, arrow, right. + """ + # order matters -- need to try <-> first + arrows = ('<->', '->') + for arrow in arrows: + left, a, right = eq.partition(arrow) + if a != '': + return left, a, right + + return eq, '', '' + + def chemical_equations_equal(eq1, eq2, exact=False): """ - Check whether two chemical equations are the same. + Check whether two chemical equations are the same. (equations have arrows) If exact is False, then they are considered equal if they differ by a constant factor. @@ -333,19 +393,13 @@ def chemical_equations_equal(eq1, eq2, exact=False): If there's a syntax error, we raise pyparsing.ParseException. """ - # for now, we do a manual parse for the arrow. - arrows = ('<->', '->') # order matters -- need to try <-> first - def split_on_arrow(s): - """Split a string on an arrow. Returns left, arrow, right, or raises ParseException if there isn't an arrow""" - for arrow in arrows: - left, a, right = s.partition(arrow) - if a != '': - return left, a, right - raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows)) left1, arrow1, right1 = split_on_arrow(eq1) left2, arrow2, right2 = split_on_arrow(eq2) + if arrow1 == '' or arrow2 == '': + raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows)) + # TODO: may want to be able to give student helpful feedback about why things didn't work. if arrow1 != arrow2: # arrows don't match diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 49cc91f343..220c606daf 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -708,3 +708,34 @@ def imageinput(element, value, status, render_template, msg=''): return etree.XML(html) _reg(imageinput) + + +#-------------------------------------------------------------------------------- + + +class ChemicalEquationInput(InputTypeBase): + """ + An input type for entering chemical equations. Supports live preview. + + Example: + + + + options: size -- width of the textbox. + """ + + template = "chemicalequationinput.html" + tags = ['chemicalequationinput'] + + def _get_render_context(self): + size = self.xml.get('size', '20') + context = { + 'id': self.id, + 'value': self.value, + 'status': self.status, + 'size': size, + 'previewer': '/static/js/capa/chemical_equation_preview.js', + } + return context + +register_input_class(ChemicalEquationInput) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6857d504ec..097c04fcd3 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -867,7 +867,7 @@ def sympy_check2():
"""}] response_tag = 'customresponse' - allowed_inputfields = ['textline', 'textbox'] + allowed_inputfields = ['textline', 'textbox', 'chemicalequationinput'] def setup_response(self): xml = self.xml diff --git a/common/lib/capa/capa/templates/chemicalequationinput.html b/common/lib/capa/capa/templates/chemicalequationinput.html new file mode 100644 index 0000000000..f705ec3d06 --- /dev/null +++ b/common/lib/capa/capa/templates/chemicalequationinput.html @@ -0,0 +1,42 @@ +
+
+ + % if status == 'unsubmitted': +
+ % elif status == 'correct': +
+ % elif status == 'incorrect': +
+ % elif status == 'incomplete': +
+ % endif + + + +

+ % if status == 'unsubmitted': + unanswered + % elif status == 'correct': + correct + % elif status == 'incorrect': + incorrect + % elif status == 'incomplete': + incomplete + % endif +

+ +
+ +
+ + +

+ +% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +
+% endif +
diff --git a/common/static/js/capa/README b/common/static/js/capa/README new file mode 100644 index 0000000000..bb698ef00e --- /dev/null +++ b/common/static/js/capa/README @@ -0,0 +1 @@ +These files really should be in the capa module, but we don't have a way to load js from there at the moment. (TODO) diff --git a/common/static/js/capa/chemical_equation_preview.js b/common/static/js/capa/chemical_equation_preview.js new file mode 100644 index 0000000000..9c5c6cd6bc --- /dev/null +++ b/common/static/js/capa/chemical_equation_preview.js @@ -0,0 +1,12 @@ +(function () { + var preview_div = $('.chemicalequationinput .equation'); + $('.chemicalequationinput input').bind("input", function(eventObject) { + $.get("/preview/chemcalc/", {"formula" : this.value}, function(response) { + if (response.error) { + preview_div.html("" + response.error + ""); + } else { + preview_div.html(response.preview); + } + }); + }); +}).call(this); diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 22ab6df67b..1e45822ebf 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,6 +1,7 @@ import hashlib import json import logging +import pyparsing import sys from django.conf import settings @@ -13,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt from requests.auth import HTTPBasicAuth from capa.xqueue_interface import XQueueInterface +from capa.chem import chemcalc from courseware.access import has_access from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache @@ -471,3 +473,42 @@ def modx_dispatch(request, dispatch, location, course_id): # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) + +def preview_chemcalc(request): + """ + Render an html preview of a chemical formula or equation. The fact that + this is here is a bit of hack. See the note in lms/urls.py about why it's + here. (Victor is to blame.) + + request should be a GET, with a key 'formula' and value 'some formula string'. + + Returns a json dictionary: + { + 'preview' : 'the-preview-html' or '' + 'error' : 'the-error' or '' + } + """ + if request.method != "GET": + raise Http404 + + result = {'preview': '', + 'error': '' } + formula = request.GET.get('formula') + if formula is None: + result['error'] = "No formula specified." + + return HttpResponse(json.dumps(result)) + + try: + result['preview'] = chemcalc.render_to_html(formula) + except pyparsing.ParseException as p: + result['error'] = "Couldn't parse formula: {0}".format(p) + except Exception: + # this is unexpected, so log + log.warning("Error while previewing chemical formula", exc_info=True) + result['error'] = "Error while rendering preview" + + return HttpResponse(json.dumps(result)) + + + diff --git a/lms/urls.py b/lms/urls.py index 662e41235e..2ea02e25c2 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -141,6 +141,16 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch', name='modx_dispatch'), + + # TODO (vshnayder): This is a hack. It creates a direct connection from + # the LMS to capa functionality, and really wants to go through the + # input types system so that previews can be context-specific. + # Unfortunately, we don't have time to think through the right way to do + # that (and implement it), and it's not a terrible thing to provide a + # generic chemican-equation rendering service. + url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc', + name='preview_chemcalc'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback', name='xqueue_callback'), From b6f7427e2290fc0e86d60b6445aadf63f4a8b589 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 12 Oct 2012 14:02:22 -0400 Subject: [PATCH 087/143] make tests pass again --- common/lib/capa/capa/chem/tests.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py index 7f9ceba6e0..9d415ad402 100644 --- a/common/lib/capa/capa/chem/tests.py +++ b/common/lib/capa/capa/chem/tests.py @@ -212,63 +212,63 @@ class Test_Render_Equations(unittest.TestCase): def test_render1(self): s = "H2O + CO2" out = render_to_html(s) - correct = "H2O+CO2" + correct = u'H2O+CO2' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render_uncorrect_reaction(self): s = "O2C + OH2" out = render_to_html(s) - correct = "O2C+OH2" + correct = u'O2C+OH2' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render2(self): s = "CO2 + H2O + Fe(OH)3" out = render_to_html(s) - correct = "CO2+H2O+Fe(OH)3" + correct = u'CO2+H2O+Fe(OH)3' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render3(self): s = "3H2O + 2CO2" out = render_to_html(s) - correct = "3H2O+2CO2" + correct = u'3H2O+2CO2' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render4(self): s = "H^+ + OH^-" out = render_to_html(s) - correct = "H++OH-" + correct = u'H++OH-' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render5(self): s = "Fe(OH)^2- + (OH)^-" out = render_to_html(s) - correct = "Fe(OH)2-+(OH)-" + correct = u'Fe(OH)2-+(OH)-' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render6(self): s = "7/2H^+ + 3/5OH^-" out = render_to_html(s) - correct = "72H++35OH-" + correct = u'72H++35OH-' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render7(self): s = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O" out = render_to_html(s) - correct = "5(H1H212)70010-+2H2O+72HCl+H2O" + correct = u'5(H1H212)70010-+2H2O+72HCl+H2O' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render8(self): s = "H2O(s) + CO2" out = render_to_html(s) - correct = "H2O(s)+CO2" + correct = u'H2O(s)+CO2' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) @@ -276,18 +276,21 @@ class Test_Render_Equations(unittest.TestCase): s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" #import ipdb; ipdb.set_trace() out = render_to_html(s) - correct = "5[Ni(NH3)4]2++52SO42-" + correct = u'5[Ni(NH3)4]2++52SO42-' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) def test_render_error(self): s = "5.2H20" - self.assertRaises(ParseException, render_to_html, s) + out = render_to_html(s) + correct = u'5.2H20' + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) def test_render_simple_brackets(self): s = "(Ar)" out = render_to_html(s) - correct = "(Ar)" + correct = u'(Ar)' log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) From f6f2663b77b6ba91a0d31684bf236e4726cdf346 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 12 Oct 2012 14:08:21 -0400 Subject: [PATCH 088/143] fix arrows list --- common/lib/capa/capa/chem/chemcalc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/chem/chemcalc.py b/common/lib/capa/capa/chem/chemcalc.py index 79a788404d..a58a3ac85d 100644 --- a/common/lib/capa/capa/chem/chemcalc.py +++ b/common/lib/capa/capa/chem/chemcalc.py @@ -14,6 +14,7 @@ from pyparsing import (Literal, Keyword, Word, nums, StringEnd, Optional, import nltk from nltk.tree import Tree +ARROWS = ('<->', '->') ## Defines a simple pyparsing tokenizer for chemical equations elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be', @@ -363,8 +364,7 @@ def split_on_arrow(eq): Return left, arrow, right. """ # order matters -- need to try <-> first - arrows = ('<->', '->') - for arrow in arrows: + for arrow in ARROWS: left, a, right = eq.partition(arrow) if a != '': return left, a, right @@ -398,7 +398,7 @@ def chemical_equations_equal(eq1, eq2, exact=False): left2, arrow2, right2 = split_on_arrow(eq2) if arrow1 == '' or arrow2 == '': - raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows)) + raise ParseException("Could not find arrow. Legal arrows: {0}".format(ARROWS)) # TODO: may want to be able to give student helpful feedback about why things didn't work. if arrow1 != arrow2: From 01ac01b62ca00ba99140ef4c32633935e68cb33f Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 12 Oct 2012 14:31:20 -0400 Subject: [PATCH 089/143] Re-enable change name, and fix up change email styles --- common/djangoapps/student/views.py | 25 +++-- lms/templates/dashboard.html | 103 +++++++++++++++------ lms/templates/email_change_successful.html | 2 +- lms/urls.py | 2 +- 4 files changed, 93 insertions(+), 39 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index bca8ff9d76..a2369574d2 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -672,9 +672,12 @@ def change_name_request(request): pnc.rationale = request.POST['rationale'] if len(pnc.new_name) < 2: return HttpResponse(json.dumps({'success': False, 'error': 'Name required'})) - if len(pnc.rationale) < 2: - return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'})) pnc.save() + + # The following automatically accepts name change requests. Remove this to + # go back to the old system where it gets queued up for admin approval. + accept_name_change_by_id(pnc.id) + return HttpResponse(json.dumps({'success': True})) @@ -709,14 +712,9 @@ def reject_name_change(request): return HttpResponse(json.dumps({'success': True})) -@ensure_csrf_cookie -def accept_name_change(request): - ''' JSON: Name change process. Course staff clicks 'accept' on a given name change ''' - if not request.user.is_staff: - raise Http404 - +def accept_name_change_by_id(id): try: - pnc = PendingNameChange.objects.get(id=int(request.POST['id'])) + pnc = PendingNameChange.objects.get(id=id) except PendingNameChange.DoesNotExist: return HttpResponse(json.dumps({'success': False, 'error': 'Invalid ID'})) @@ -735,3 +733,12 @@ def accept_name_change(request): pnc.delete() return HttpResponse(json.dumps({'success': True})) + + +@ensure_csrf_cookie +def accept_name_change(request): + ''' JSON: Name change process. Course staff clicks 'accept' on a given name change ''' + if not request.user.is_staff: + raise Http404 + + return accept_name_change_by_id(int(request.POST['id'])) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 9b59dc2cd9..3fdcbb5c1c 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -46,16 +46,33 @@ function(data) { if (data.success) { $("#change_email_title").html("Please verify your new email"); - $("#change_email_body").html("

You'll receive a confirmation in your " + + $("#change_email_form").html("

You'll receive a confirmation in your " + "in-box. Please click the link in the " + "email to confirm the email change.

"); } else { - $("#change_email_error").html(data.error); + $("#change_email_error").html(data.error).stop().css("display", "block"); } }); return false; }); + $("#change_name_form").submit(function(){ + var new_name = $('#new_name_field').val(); + var rationale = $('#name_rationale_field').val(); + + $.post('${reverse("change_name")}', + {"new_name":new_name, "rationale":rationale}, + function(data) { + if(data.success) { + location.reload(); + // $("#change_name_body").html("

Name changed.

"); + } else { + $("#change_name_error").html(data.error).stop().css("display", "block"); + } + }); + return false; + }); + })(this) @@ -75,13 +92,13 @@ diff --git a/lms/templates/static_templates/press_releases/UT_joins_edX.html b/lms/templates/static_templates/press_releases/UT_joins_edX.html new file mode 100644 index 0000000000..890789efc7 --- /dev/null +++ b/lms/templates/static_templates/press_releases/UT_joins_edX.html @@ -0,0 +1,110 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">The University of Texas System joins edX +
+ + +
+
+

The University of Texas System joins edX

+
+
+

The University of Texas System joins Harvard, MIT and UC Berkeley in not-for-profit online learning collaborative

+ +

CAMBRIDGE, MA/AUSTIN, TX – October 15, 2012 — edX, the online non-profit learning initiative founded by Harvard University (Harvard) and the Massachusetts Institute of Technology (MIT) and launched in May, announced today the addition of The University of Texas (UT) System to its platform. The UT System, one of the largest public university systems in the United States with nine academic universities and six health institutions, will collaborate with edX to expand the group of participating “X Universities” – universities offering their courses on the edX platform.

+ +

The UT System includes the University of Texas at Austin, ranked 25th in the 2012-2013 Times Higher Education World University Rankings, UT Southwestern Medical Center, home to one of the nation's top 25 medical schools, and UT MD Anderson Cancer Center, the nation's No. 1-ranked cancer center. The system's institutions serve 212,000 students and employ 19,000 faculty members.

+ +

Through edX, the “X Universities” provide online interactive education wherever there is access to the Internet, with a goal to enhance teaching and learning through research about how students learn, and how technologies can facilitate effective teaching both on campus and online. The University of California, Berkeley (UC Berkeley) joined edX in July 2012. edX plans to add other “X Universities” from around the world to the edX platform in the coming months.

+ +

Francisco G. Cigarroa, Chancellor of The University of Texas System announced the partnership following a unanimous vote of approval by the UT System's Board of Regents on Monday.

+ +

“New technologies are positively impacting how professors teach and how course content is delivered,” Chancellor Cigarroa said. “The University of Texas System will help lead this revolution and fundamentally alter the direction of online education. We are excited about this partnership with edX and honored to be in the company of such exceptional institutions as MIT, Harvard and Berkeley. The mission of edX aligns perfectly with that of the UT System and keeps the learner as its central focus.”

+ +

The University of Texas System plans to offer at least four courses on edX within the next year.

+ +

In addition to serving a global community of online students, the UT System plans to redesign general education courses and traditional entry-level courses that are too often made up of several hundred students. Through its Institute for Transformational Learning, the UT System plans to give students more options by offering courses that are customized to student needs. For example, the UT System plans to offer courses that use a combination of technology and face-to-face interaction, courses that allow students to manage their own time by accelerating through sections they have already mastered or spending more time on areas they find challenging, and fully online courses so students are not limited by their location.

+ +

“As Texas' flagship university, UT Austin is committed not only to embracing breakthroughs in education, but helping create them,” said William Powers, Jr., President of UT Austin. “We're proud to be partnering with these top peer universities on edX.”

+ +

As part of a bold and innovative plan, the UT System also plans to offer courses through edX that will allow students to earn college credits toward a degree. “Our goal through our partnership with edX is to better meet the learning needs of a wide range of students, raise graduation rates and cut the cost of higher education, all while maintaining our commitment to education of the highest quality,” said Gene Powell, chairman of the UT System Board of Regents.

+ +

The UT System brings a large and diverse student body to the edX family. Its six health institutions offer a unique opportunity to provide groundbreaking health and medical courses via edX in the near future. The UT System also brings special expertise in analytics – assessing student learning, online course design and creating interactive learning environments.

+ +

edX courses are designed to provide students with a wealth of innovative resources, including interactive laboratories, virtual reality environments and access to online tutors and tutorials. Students who take UT System courses through edX won't work in isolation, but will have the opportunity to participate in online forums, network with instructors and fellow students and take part in exciting collaborative projects. “We are excited that The University of Texas System is joining edX's efforts to revolutionize learning,” said Anant Agarwal, President of edX. “The institutions within The University of Texas System bring a wide range of expertise to the edX mission, and with them edX is now positioned to continue to increase our offering of high-quality, online courses.”

+ +

edX was created by Harvard and MIT in May, with each university committing to contribute $30 million toward the online partnership.

+ +

“Today's announcement is another important step toward our shared objectives of expanding access to high quality educational content while enhancing teaching and learning online and in the classroom,” said Harvard President Drew Faust. “The addition of The University of Texas System to the edX platform will allow us to deepen our understanding of learning, develop new approaches to teaching that build on that knowledge, and strengthen both the on-campus and online learning experience.” + +

“At MIT, we are energetically exploring the ways that online instruction can help us reimagine our campus residential education even as it allows us to reach an unprecedented number of learners around the world,” said MIT President L. Rafael Reif. “It is thrilling to be joined by The University of Texas System in the pursuit of that dual goal.”

+ +

The edX classes to be offered by the UT System will be announced soon and will join other new edX courses planned for Spring, Summer and Fall 2013. As with all edX courses, online learners who obtain a passing grade in the UT System courses will receive a certificate of mastery. edX will also offer the option of proctored examinations for the UT System courses.

+ + +

About edX

+ +

edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT's Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning—both on-campus and worldwide. edX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.

+ +

About Harvard University

+ +

Harvard University is devoted to excellence in teaching, learning and research, and to developing leaders in many disciplines who make a difference globally. Harvard Faculty are engaged with teaching and research to push the boundaries of human knowledge. The University has twelve degree-granting Schools in addition to the Radcliffe Institute for Advanced Study.

+ +

Established in 1636, Harvard is the oldest institution of higher education in the United States. The University, which is based in Cambridge and Boston, Massachusetts, has an enrollment of over 20,000 degree candidates, including undergraduate, graduate and professional students. Harvard has more than 360,000 alumni around the world.

+ +

About MIT

+

The Massachusetts Institute of Technology — a coeducational, privately endowed research university founded in 1861 — is dedicated to advancing knowledge and educating students in science, technology and other areas of scholarship that will best serve the nation and the world in the 21st century. The Institute has close to 1,000 faculty and 10,000 undergraduate and graduate students. It is organized into five Schools: Architecture and Urban Planning; Engineering; Humanities, Arts, and Social Sciences; Sloan School of Management; and Science.

+ +

MIT's commitment to innovation has led to a host of scientific breakthroughs and technological advances. Achievements of the Institute's faculty and graduates have included the first chemical synthesis of penicillin and vitamin A, the development of inertial guidance systems, modern technologies for artificial limbs and the magnetic core memory that made possible the development of digital computers. Seventy-eight alumni, faculty, researchers and staff have won Nobel Prizes.

+ +

Current areas of research and education include neuroscience and the study of the brain and mind, bioengineering, cancer, energy, the environment and sustainable development, information sciences and technology, new media, financial technology and entrepreneurship.

+ +

About the University of California, Berkeley

+ +

The University of California, Berkeley is the world's premier public university with a mission to excel in teaching, research and public service. This longstanding mission has led to the university's distinguished record of Nobel-level scholarship, constant innovation, a concern for the betterment of our world, and consistently high rankings of its schools and departments. The campus offers superior, high value education for extraordinarily talented students from all walks of life; operational excellence and a commitment to the competitiveness and prosperity of California and the nation.

+ +

The University of California was chartered in 1868 and its flagship campus in Berkeley, on San Francisco Bay, was envisioned as a “City of Learning.” Today, there are more than 1,500 fulltime and 500 part-time faculty members dispersed among more than 130 academic departments and more than 80 interdisciplinary research units. Twenty-two Nobel Prizes have been garnered by faculty and 28 by UC Berkeley alumni. There are 9 Nobel Laureates, 32 MacArthur Fellows, and 4 Pulitzer Prize winners among the current faculty.

+ +

About The University of Texas System

+ +

Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state's undergraduate degrees and educates nearly three-fourths of the state's health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state. www.utsystem.edu

+ +
+

edX Contact: Dan O’Connell

+

oconnell@edx.org

+

617-480-6585

+
+

UT System Contact: Jenny LaCoste-Caputo

+

jcaputo@utsystem.edu

+

512-499-4361

+
+ + +
+
+
diff --git a/lms/templates/university_profile/utx.html b/lms/templates/university_profile/utx.html new file mode 100644 index 0000000000..756c9dc62b --- /dev/null +++ b/lms/templates/university_profile/utx.html @@ -0,0 +1,24 @@ +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> + +<%block name="title">UTx + +<%block name="university_header"> + + + + +<%block name="university_description"> +

Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.

+ + +${parent.body()} diff --git a/lms/urls.py b/lms/urls.py index 035db95596..89a541ab06 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -52,6 +52,7 @@ urlpatterns = ('', url(r'^heartbeat$', include('heartbeat.urls')), + url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'UTx'}), url(r'^university_profile/(?P[^/]+)$', 'courseware.views.university_profile', name="university_profile"), #Semi-static views (these need to be rendered and have the login bar, but don't change) @@ -88,6 +89,8 @@ urlpatterns = ('', {'template': 'press_releases/edX_announces_proctored_exam_testing.html'}, name="press/edX-announces-proctored-exam-testing"), url(r'^press/elsevier-collaborates-with-edx$', 'static_template_view.views.render', {'template': 'press_releases/Elsevier_collaborates_with_edX.html'}, name="press/elsevier-collaborates-with-edx"), + url(r'^press/ut-joins-edx$', 'static_template_view.views.render', + {'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"), # Should this always update to point to the latest press release? From 516daa47107e5af664ddf9c5e37bb2ec0f6d48a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 15 Oct 2012 06:58:39 -0400 Subject: [PATCH 109/143] Converted FAQ template to unix line endings --- lms/templates/static_templates/faq.html | 234 ++++++++++++------------ 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index 2a9df7c5fc..d0f2191b8f 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -1,117 +1,117 @@ -<%! from django.core.urlresolvers import reverse %> -<%namespace name='static' file='../static_content.html'/> - -<%inherit file="../main.html" /> - -<%block name="title">FAQ - -
- -
-
-
-

Organization

-
-

What is edX?

- -

Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

-

EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

-
-
-

What are "X Universities"?

-

Harvard, MIT and UC Berkeley, as the first universities whose courses are delivered on the edX website, are "X Universities." The three institutions will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities" as soon as possible. Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

-
-
-

Why is UC Berkeley joining edX?

-

Like Harvard and MIT, UC Berkeley seeks to transform education in quality, efficiency and scale through technology and research, for the benefit of campus-based students and the global community of online learners.

-

UC Berkeley also shares the edX commitment to the not-for-profit and open-platform model as a way to enhance human fulfillment worldwide.

-
-
-

What will UC Berkeley's direct participation entail?

-

UC Berkeley will begin by offering two courses on edX in Fall 2012, and will collaborate on the development of the technology platform. We will explore, experiment and innovate together.

-

UC Berkeley will also serve as the inaugural chair of the "X University" Consortium for an initial 5 year period. As Chair, UC Berkeley will participate on the edX Board on behalf of the X Universities.

-
-
-

Why is The University of Texas System joining edX?

-

Joining edX not only allows UT faculty to showcase their work on a global stage, but also provides UT students the opportunity to take classes from their choice of UT institutions, as well as MIT, Harvard, UC Berkeley and future “X” Universities.

-

The UT System closely examined all the alternatives and determined that edX offered the best fit in terms of alignment of mission, platform and revenue model. The strength and reputation of the partner institutions – MIT, Harvard and UC Berkeley – was also a huge consideration. EdX is committed to both blended and online learning and to a non-profit, open source model. It is also governed by a board of academics with a commitment to excellence in learning.

-
-
-

What will The UT System’s direct participation entail?

-

The UT System will begin by offering one course on edX from The University of Texas at Austin in Summer 2013, and four courses in Fall 2013, likely at least one of those courses from one of its health institutions. The UT System is also making a $5 million investment in the edX platform. We will explore, experiment and innovate together.

-
-
-

Will edX be adding additional X Universities?

-

More than 140 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the “X University” Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley and the UT System will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities” as soon as possible. Each member of the consortium will offer courses on the edX platform as an “X University.” The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

-

EdX will actively explore the addition of other institutions from around the world to the edX platform, and we look forward to adding more “X Universities” as capacity increases.

-
-
- -
-

Students

-
-

Who can take edX courses? Will there be an admissions process?

-

EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

-
-
-

Will certificates be awarded?

-

Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

-
-
-

What will the scope of the online courses be? How many? Which faculty?

-

Our goal is to offer a wide variety of courses across disciplines. There are currently seven courses offered for Fall 2012.

-
-
-

Who is the learner? Domestic or international? Age range?

-

Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

-
-
-

Will participating universities’ standards apply to all courses offered on the edX platform?

-

Yes: the reach changes exponentially, but the rigor remains the same.

-
-
-

How do you intend to test whether this approach is improving learning?

-

Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

-
-
-

How may I apply to study with edX?

-

Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

-
-
-

How may another university participate in edX?

-

If you are from a university interested in discussing edX, please email university@edx.org

-
-
- -
-

Technology Platform

-
-

What technology will edX use?

-

The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

-

The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

-
-
-

How is this different from what other universities are doing online?

-

EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

-
-
- -
- - -
-
- -%if user.is_authenticated(): - <%include file="../signup_modal.html" /> -%endif +<%! from django.core.urlresolvers import reverse %> +<%namespace name='static' file='../static_content.html'/> + +<%inherit file="../main.html" /> + +<%block name="title">FAQ + +
+ +
+
+
+

Organization

+
+

What is edX?

+ +

Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

+

EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

+
+
+

What are "X Universities"?

+

Harvard, MIT and UC Berkeley, as the first universities whose courses are delivered on the edX website, are "X Universities." The three institutions will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities" as soon as possible. Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

+
+
+

Why is UC Berkeley joining edX?

+

Like Harvard and MIT, UC Berkeley seeks to transform education in quality, efficiency and scale through technology and research, for the benefit of campus-based students and the global community of online learners.

+

UC Berkeley also shares the edX commitment to the not-for-profit and open-platform model as a way to enhance human fulfillment worldwide.

+
+
+

What will UC Berkeley's direct participation entail?

+

UC Berkeley will begin by offering two courses on edX in Fall 2012, and will collaborate on the development of the technology platform. We will explore, experiment and innovate together.

+

UC Berkeley will also serve as the inaugural chair of the "X University" Consortium for an initial 5 year period. As Chair, UC Berkeley will participate on the edX Board on behalf of the X Universities.

+
+
+

Why is The University of Texas System joining edX?

+

Joining edX not only allows UT faculty to showcase their work on a global stage, but also provides UT students the opportunity to take classes from their choice of UT institutions, as well as MIT, Harvard, UC Berkeley and future “X” Universities.

+

The UT System closely examined all the alternatives and determined that edX offered the best fit in terms of alignment of mission, platform and revenue model. The strength and reputation of the partner institutions – MIT, Harvard and UC Berkeley – was also a huge consideration. EdX is committed to both blended and online learning and to a non-profit, open source model. It is also governed by a board of academics with a commitment to excellence in learning.

+
+
+

What will The UT System’s direct participation entail?

+

The UT System will begin by offering one course on edX from The University of Texas at Austin in Summer 2013, and four courses in Fall 2013, likely at least one of those courses from one of its health institutions. The UT System is also making a $5 million investment in the edX platform. We will explore, experiment and innovate together.

+
+
+

Will edX be adding additional X Universities?

+

More than 140 institutions from around the world have expressed interest in collaborating with edX since Harvard and MIT announced its creation in May. EdX is focused above all on quality and developing the best not-for-profit model for online education. In addition to providing online courses on the edX platform, the “X University” Consortium will be a forum in which members can share experiences around online learning. Harvard, MIT, UC Berkeley and the UT System will work collaboratively to establish the “X University” Consortium, whose membership will expand to include additional “X Universities” as soon as possible. Each member of the consortium will offer courses on the edX platform as an “X University.” The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

+

EdX will actively explore the addition of other institutions from around the world to the edX platform, and we look forward to adding more “X Universities” as capacity increases.

+
+
+ +
+

Students

+
+

Who can take edX courses? Will there be an admissions process?

+

EdX will be available to anyone in the world with an internet connection, and in general, there will not be an admissions process.

+
+
+

Will certificates be awarded?

+

Yes. Online learners who demonstrate mastery of subjects can earn a certificate of completion. Certificates will be issued by edX under the name of the underlying "X University" from where the course originated, i.e. HarvardX, MITx or BerkeleyX. For the courses in Fall 2012, those certificates will be free. There is a plan to charge a modest fee for certificates in the future.

+
+
+

What will the scope of the online courses be? How many? Which faculty?

+

Our goal is to offer a wide variety of courses across disciplines. There are currently seven courses offered for Fall 2012.

+
+
+

Who is the learner? Domestic or international? Age range?

+

Improving teaching and learning for students on our campuses is one of our primary goals. Beyond that, we don’t have a target group of potential learners, as the goal is to make these courses available to anyone in the world – from any demographic – who has interest in advancing their own knowledge. The only requirement is to have a computer with an internet connection. More than 150,000 students from over 160 countries registered for MITx's first course, 6.002x: Circuits and Electronics. The age range of students certified in this course was from 14 to 74 years-old.

+
+
+

Will participating universities’ standards apply to all courses offered on the edX platform?

+

Yes: the reach changes exponentially, but the rigor remains the same.

+
+
+

How do you intend to test whether this approach is improving learning?

+

Edx institutions have assembled faculty members who will collect and analyze data to assess results and the impact edX is having on learning.

+
+
+

How may I apply to study with edX?

+

Simply complete the online signup form. Enrolling will create your unique student record in the edX database, allow you to register for classes, and to receive a certificate on successful completion.

+
+
+

How may another university participate in edX?

+

If you are from a university interested in discussing edX, please email university@edx.org

+
+
+ +
+

Technology Platform

+
+

What technology will edX use?

+

The edX open-source online learning platform will feature interactive learning designed specifically for the web. Features will include: self-paced learning, online discussion groups, wiki-based collaborative learning, assessment of learning as a student progresses through a course, and online laboratories and other interactive learning tools. The platform will also serve as a laboratory from which data will be gathered to better understand how students learn. Because it is open source, the platform will be continuously improved by a worldwide community of collaborators, with new features added as needs arise.

+

The first version of the technology was used in the first MITx course, 6.002x Circuits and Electronics, which launched in Spring, 2012.

+
+
+

How is this different from what other universities are doing online?

+

EdX is a not-for-profit enterprise built upon the shared educational missions of its founding partners, Harvard University and MIT. The edX platform will be available as open source. Also, a primary goal of edX is to improve teaching and learning on campus by experimenting with blended models of learning and by supporting faculty in conducting significant research on how students learn.

+
+
+ +
+ + +
+
+ +%if user.is_authenticated(): + <%include file="../signup_modal.html" /> +%endif From 40dd6fa10510f0c4ff659600777c4d5c9bbb20c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 15 Oct 2012 07:16:13 -0400 Subject: [PATCH 110/143] Add missing text to FAQ --- lms/templates/static_templates/faq.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/templates/static_templates/faq.html b/lms/templates/static_templates/faq.html index d0f2191b8f..7290df165e 100644 --- a/lms/templates/static_templates/faq.html +++ b/lms/templates/static_templates/faq.html @@ -18,8 +18,7 @@

Organization

What is edX?

- -

Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

+

edX is a not-for-profit enterprise of its founding partners, the Massachusetts Institute of Technology (MIT) and Harvard University that offers online learning to on-campus students and to millions of people around the world. To do so, edX is building an open-source online learning platform and hosts an online web portal at www.edx.org for online education.

EdX currently offers HarvardX, MITx and BerkeleyX classes online for free. Beginning in Summer 2013, edX will also offer UTx (University of Texas) classes online for free. The University of Texas System includes nine universities and six health institutions. The edX institutions aim to extend their collective reach to build a global community of online students. Along with offering online courses, the three universities undertake research on how students learn and how technology can transform learning – both on-campus and online throughout the world.

From b13412d3b22d030c6a9737276d78b7b067c40ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 15 Oct 2012 11:12:22 -0400 Subject: [PATCH 111/143] Make corrections of UT announcement --- .../images/university/ut/ut-rollover_160x90.png | Bin 0 -> 12266 bytes .../university/ut/ut-standalone_187x80.png | Bin 0 -> 14625 bytes lms/templates/feed.rss | 3 ++- lms/templates/index.html | 2 +- lms/templates/university_profile/utx.html | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 lms/static/images/university/ut/ut-rollover_160x90.png create mode 100644 lms/static/images/university/ut/ut-standalone_187x80.png diff --git a/lms/static/images/university/ut/ut-rollover_160x90.png b/lms/static/images/university/ut/ut-rollover_160x90.png new file mode 100644 index 0000000000000000000000000000000000000000..4c4422d5447839022e88c8f7754ce284b55cbdb8 GIT binary patch literal 12266 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 t8<}Jz!|#_ZkY4|}B+^zO6C*_iV5_G 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 AX#fBK literal 0 HcmV?d00001 diff --git a/lms/templates/feed.rss b/lms/templates/feed.rss index fe4b620709..56ca08182e 100644 --- a/lms/templates/feed.rss +++ b/lms/templates/feed.rss @@ -13,7 +13,8 @@ 2012-10-14T14:00:00-07:00 The University of Texas System joins edX - <img src="${static.url('images/press/uts-seal_109x84.jpg')}" /> + <img src="${static.url('images/press/uts-seal_109x84.jpg')}" /> + <p>nine universities and six health institutions</p> diff --git a/lms/templates/index.html b/lms/templates/index.html index 96af61aad8..b1d9925416 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -67,7 +67,7 @@
  • - +
    UTx
    diff --git a/lms/templates/university_profile/utx.html b/lms/templates/university_profile/utx.html index 756c9dc62b..b9378f6ce3 100644 --- a/lms/templates/university_profile/utx.html +++ b/lms/templates/university_profile/utx.html @@ -8,7 +8,7 @@
  • -
    -

    What are "X Universities"?

    -

    Harvard, MIT and UC Berkeley, as the first universities whose courses are delivered on the edX website, are "X Universities." The three institutions will work collaboratively to establish the "X University" Consortium, whose membership will expand to include additional "X Universities" as soon as possible. Each member of the consortium will offer courses on the edX platform as an "X University." The gathering of many universities’ educational content together on one site will enable learners worldwide to access the course content of any participating university from a single website, and to use a set of online educational tools shared by all participating universities.

    -
    -
    -

    Why is UC Berkeley joining edX?

    -

    Like Harvard and MIT, UC Berkeley seeks to transform education in quality, efficiency and scale through technology and research, for the benefit of campus-based students and the global community of online learners.

    -

    UC Berkeley also shares the edX commitment to the not-for-profit and open-platform model as a way to enhance human fulfillment worldwide.

    -
    -
    -

    What will UC Berkeley's direct participation entail?

    -

    UC Berkeley will begin by offering two courses on edX in Fall 2012, and will collaborate on the development of the technology platform. We will explore, experiment and innovate together.

    -

    UC Berkeley will also serve as the inaugural chair of the "X University" Consortium for an initial 5 year period. As Chair, UC Berkeley will participate on the edX Board on behalf of the X Universities.

    -

    Why is The University of Texas System joining edX?

    Joining edX not only allows UT faculty to showcase their work on a global stage, but also provides UT students the opportunity to take classes from their choice of UT institutions, as well as MIT, Harvard, UC Berkeley and future “X” Universities.

    From 57f49353d8cad3cbce25730272fd122ab91eb005 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 15 Oct 2012 22:09:19 +0000 Subject: [PATCH 116/143] Fix CR showanswer template --- common/lib/capa/capa/templates/textbox.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/textbox.html index 91aa6d41c8..6e44712d9f 100644 --- a/common/lib/capa/capa/templates/textbox.html +++ b/common/lib/capa/capa/templates/textbox.html @@ -5,8 +5,6 @@ % endif >${value|h} - -
    % if state == 'unsubmitted': Unanswered @@ -26,6 +24,8 @@

    ${state}

    + +
    ${msg|n}
    From 24d5c9162c8bceddca1b6b19bf3af1c42f0f7e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 21 Aug 2012 10:20:26 -0400 Subject: [PATCH 117/143] Created Django App with commands to import software license numbers per class. --- lms/djangoapps/licenses/__init__.py | 0 .../licenses/management/__init__.py | 0 .../licenses/management/commands/__init__.py | 0 .../management/commands/import_serials.py | 77 +++++++++++++++++++ lms/djangoapps/licenses/models.py | 19 +++++ lms/djangoapps/licenses/views.py | 0 lms/envs/common.py | 1 + 7 files changed, 97 insertions(+) create mode 100644 lms/djangoapps/licenses/__init__.py create mode 100644 lms/djangoapps/licenses/management/__init__.py create mode 100644 lms/djangoapps/licenses/management/commands/__init__.py create mode 100644 lms/djangoapps/licenses/management/commands/import_serials.py create mode 100644 lms/djangoapps/licenses/models.py create mode 100644 lms/djangoapps/licenses/views.py diff --git a/lms/djangoapps/licenses/__init__.py b/lms/djangoapps/licenses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/licenses/management/__init__.py b/lms/djangoapps/licenses/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/licenses/management/commands/__init__.py b/lms/djangoapps/licenses/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/licenses/management/commands/import_serials.py b/lms/djangoapps/licenses/management/commands/import_serials.py new file mode 100644 index 0000000000..c65cc356f9 --- /dev/null +++ b/lms/djangoapps/licenses/management/commands/import_serials.py @@ -0,0 +1,77 @@ +import os.path + +from optparse import make_option + +from django.utils.html import escape +from django.core.management.base import BaseCommand, CommandError + +from xmodule.modulestore.django import modulestore + +from licenses.models import Software, StudentLicense + + +class Command(BaseCommand): + help = """Imports serial numbers for software used in a course. + + Usage: import_serials course_id software_id serial_file + + serial_file is a text file that list one available serial number per line. + + Example: + import_serials.py MITx/6.002x/2012_Fall matlab /tmp/matlab-serials.txt + """ + + args = "course_id software_id serial_file" + + def handle(self, *args, **options): + """ + """ + course_id, software_name, filename = self._parse_arguments(args) + + software = self._find_software(course_id, software_name) + + self._import_serials(software, filename) + + def _parse_arguments(self, args): + if len(args) != 3: + raise CommandError("Incorrect number of arguments") + + course_id = args[0] + courses = modulestore().get_courses() + known_course_ids = set(c.id for c in courses) + + if course_id not in known_course_ids: + raise CommandError("Unknown course_id") + + software_name = escape(args[1].lower()) + + filename = os.path.abspath(args[2]) + if not os.path.exists(filename): + raise CommandError("Cannot find filename {0}".format(filename)) + + return course_id, software_name, filename + + def _find_software(self, course_id, software_name): + try: + software = Software.objects.get(course_id=course_id, name=software_name) + except Software.DoesNotExist: + software = Software(name=software_name, course_id=course_id) + software.save() + + return software + + def _import_serials(self, software, filename): + print "Importing serial numbers for {0} {1}".format( + software.name, software.course_id) + + known_serials = set(l.serial for l in StudentLicense.objects.filter(software=software)) + + count = 0 + serials = list(l.strip() for l in open(filename)) + for s in serials: + if s not in known_serials: + license = StudentLicense(software=software, serial=s) + license.save() + count += 1 + + print "{0} new serial numbers imported.".format(count) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py new file mode 100644 index 0000000000..61f270c163 --- /dev/null +++ b/lms/djangoapps/licenses/models.py @@ -0,0 +1,19 @@ +""" +""" + +from django.db import models + +from student.models import User + + +class Software(models.Model): + name = models.CharField(max_length=255) + full_name = models.CharField(max_length=255) + url = models.CharField(max_length=255) + course_id = models.CharField(max_length=255) + + +class StudentLicense(models.Model): + software = models.ForeignKey(Software, db_index=True) + serial = models.CharField(max_length=255) + user = models.ForeignKey(User, null=True, blank=True) diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/envs/common.py b/lms/envs/common.py index a927da8e98..9b98e4ecfd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -626,6 +626,7 @@ INSTALLED_APPS = ( 'certificates', 'instructor', 'psychometrics', + 'licenses', #For the wiki 'wiki', # The new django-wiki from benjaoming From ed88708d716123acac5d8f9ca7304b79d363852a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 21 Aug 2012 19:04:11 -0400 Subject: [PATCH 118/143] Renamed models and commands for importing serial numbers. --- ...rt_serials.py => import_serial_numbers.py} | 44 ++++++++----------- lms/djangoapps/licenses/models.py | 13 +++--- 2 files changed, 25 insertions(+), 32 deletions(-) rename lms/djangoapps/licenses/management/commands/{import_serials.py => import_serial_numbers.py} (54%) diff --git a/lms/djangoapps/licenses/management/commands/import_serials.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py similarity index 54% rename from lms/djangoapps/licenses/management/commands/import_serials.py rename to lms/djangoapps/licenses/management/commands/import_serial_numbers.py index c65cc356f9..846327966d 100644 --- a/lms/djangoapps/licenses/management/commands/import_serials.py +++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py @@ -7,20 +7,18 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.django import modulestore -from licenses.models import Software, StudentLicense - +from licenses.models import CourseSoftware, UserLicense class Command(BaseCommand): help = """Imports serial numbers for software used in a course. - Usage: import_serials course_id software_id serial_file + Usage: import_serial_numbers serial_file is a text file that list one available serial number per line. Example: - import_serials.py MITx/6.002x/2012_Fall matlab /tmp/matlab-serials.txt + django-admin.py import_serial_numbers MITx/6.002x/2012_Fall matlab /tmp/matlab-serials.txt """ - args = "course_id software_id serial_file" def handle(self, *args, **options): @@ -28,8 +26,8 @@ class Command(BaseCommand): """ course_id, software_name, filename = self._parse_arguments(args) - software = self._find_software(course_id, software_name) - + software, _ = CourseSoftware.objects.get_or_create(course_id=course_id, + name=software_name) self._import_serials(software, filename) def _parse_arguments(self, args): @@ -51,27 +49,21 @@ class Command(BaseCommand): return course_id, software_name, filename - def _find_software(self, course_id, software_name): - try: - software = Software.objects.get(course_id=course_id, name=software_name) - except Software.DoesNotExist: - software = Software(name=software_name, course_id=course_id) - software.save() - - return software def _import_serials(self, software, filename): - print "Importing serial numbers for {0} {1}".format( - software.name, software.course_id) + print "Importing serial numbers for {0}.".format(software) - known_serials = set(l.serial for l in StudentLicense.objects.filter(software=software)) + serials = set(unicode(l.strip()) for l in open(filename)) - count = 0 - serials = list(l.strip() for l in open(filename)) - for s in serials: - if s not in known_serials: - license = StudentLicense(software=software, serial=s) - license.save() - count += 1 + # remove serial numbers we already have + licenses = UserLicense.objects.filter(software=software) + known_serials = set(l.serial for l in licenses) + if known_serials: + serials = serials.difference(known_serials) - print "{0} new serial numbers imported.".format(count) + # add serial numbers them to the database + for serial in serials: + license = UserLicense(software=software, serial=serial) + license.save() + + print "{0} new serial numbers imported.".format(len(serials)) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index 61f270c163..78da5d14cb 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -1,19 +1,20 @@ """ """ - from django.db import models - from student.models import User -class Software(models.Model): +class CourseSoftware(models.Model): name = models.CharField(max_length=255) full_name = models.CharField(max_length=255) url = models.CharField(max_length=255) course_id = models.CharField(max_length=255) + def __unicode__(self): + return u'{0} for {1}'.format(self.name, self.course_id) -class StudentLicense(models.Model): - software = models.ForeignKey(Software, db_index=True) + +class UserLicense(models.Model): + software = models.ForeignKey(CourseSoftware, db_index=True) + user = models.ForeignKey(User, null=True) serial = models.CharField(max_length=255) - user = models.ForeignKey(User, null=True, blank=True) From b2de8199b7670c795c9d91012ee9085156affe50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 21 Aug 2012 19:04:53 -0400 Subject: [PATCH 119/143] Added licenses view helper functions. --- lms/djangoapps/licenses/views.py | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index e69de29bb2..b4ab4ea909 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -0,0 +1,60 @@ +import logging +from itertools import groupby +from collections import Iterable + +from django.db.models import Q + +from models import CourseSoftware, UserLicense + +log = logging.getLogger("mitx.licenses") + + +def get_or_create_courses_licenses(user, courses): + user_licenses = get_courses_licenses(user, courses) + + for software, license in user_licenses.iteritems(): + if license is None: + user_licenses[software] = get_or_create_user_license(user, software) + + log.info(user_licenses) + + return user_licenses + + +def get_courses_licenses(user, courses): + course_ids = set(course.id for course in courses) + all_software = CourseSoftware.objects.filter(course_id__in=course_ids) + + user_licenses = dict.fromkeys(all_software, None) + + assigned_licenses = UserLicense.objects.filter(software__in=all_software, user=user) + assigned_by_software = {lic.software:lic for lic in assigned_licenses} + + for software, license in assigned_by_software.iteritems(): + user_licenses[software] = license + + return user_licenses + + +def get_or_create_user_license(user, software): + license = None + try: + # Find a licenses associated with the user or with no user + # associated. + query = (Q(user__isnull=True) | Q(user=user)) & Q(software=software) + + # TODO fix a race condition in this code when more than one + # user is getting a license assigned + + license = UserLicense.objects.filter(query)[0] + + if license.user is not user: + license.user = user + license.save() + + except IndexError: + # TODO look if someone has unenrolled from the class and already has a serial number + log.error('No serial numbers available for {0}', software) + + + return license From 516c41f342c54559cb1e82976083ee063cc6e16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 22 Aug 2012 11:53:04 -0400 Subject: [PATCH 120/143] Added html view of serial numbers. Refactored code. --- .../commands/import_serial_numbers.py | 10 +-- lms/djangoapps/licenses/models.py | 64 ++++++++++++++++- lms/djangoapps/licenses/views.py | 71 ++++++------------- lms/templates/licenses/serial_numbers.html | 10 +++ 4 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 lms/templates/licenses/serial_numbers.html diff --git a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py index 846327966d..465940ce20 100644 --- a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py @@ -9,15 +9,18 @@ from xmodule.modulestore.django import modulestore from licenses.models import CourseSoftware, UserLicense + class Command(BaseCommand): help = """Imports serial numbers for software used in a course. - Usage: import_serial_numbers + Usage: import_serial_numbers - serial_file is a text file that list one available serial number per line. + is a text file that list one available serial number per line. Example: - django-admin.py import_serial_numbers MITx/6.002x/2012_Fall matlab /tmp/matlab-serials.txt + + import_serial_numbers MITx/6.002x/2012_Fall matlab serials.txt + """ args = "course_id software_id serial_file" @@ -49,7 +52,6 @@ class Command(BaseCommand): return course_id, software_name, filename - def _import_serials(self, software, filename): print "Importing serial numbers for {0}.".format(software) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index 78da5d14cb..929fba10ec 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -1,8 +1,11 @@ -""" -""" -from django.db import models +import logging + +from django.db import models, transaction + from student.models import User +log = logging.getLogger("mitx.licenses") + class CourseSoftware(models.Model): name = models.CharField(max_length=255) @@ -18,3 +21,58 @@ class UserLicense(models.Model): software = models.ForeignKey(CourseSoftware, db_index=True) user = models.ForeignKey(User, null=True) serial = models.CharField(max_length=255) + + +def get_courses_licenses(user, courses): + course_ids = set(course.id for course in courses) + all_software = CourseSoftware.objects.filter(course_id__in=course_ids) + + assigned_licenses = UserLicense.objects.filter(software__in=all_software, + user=user) + + licenses = dict.fromkeys(all_software, None) + for license in assigned_licenses: + licenses[license.software] = license + + log.info(assigned_licenses) + log.info(licenses) + + return licenses + + +def get_license(user, software): + try: + license = UserLicense.objects.get(user=user, software=software) + except UserLicense.DoesNotExist: + license = None + + return license + + +def get_or_create_license(user, software): + license = get_license(user, software) + if license is None: + license = _create_license(user, software) + + return license + + +def _create_license(user, software): + license = None + + try: + # find one license that has not been assigned, locking the + # table/rows with select_for_update to prevent race conditions + with transaction.commit_on_success(): + selected = UserLicense.objects.select_for_update() + license = selected.filter(user__isnull=True)[0] + license.user = user + license.save() + except IndexError: + # there are no free licenses + log.error('No serial numbers available for {0}', software) + license = None + # TODO [rocha]look if someone has unenrolled from the class + # and already has a serial number + + return license diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index b4ab4ea909..7cf8e6591e 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -1,60 +1,35 @@ import logging -from itertools import groupby -from collections import Iterable +from collections import namedtuple, defaultdict -from django.db.models import Q +from mitxmako.shortcuts import render_to_string + +from models import get_courses_licenses, get_or_create_license -from models import CourseSoftware, UserLicense log = logging.getLogger("mitx.licenses") -def get_or_create_courses_licenses(user, courses): - user_licenses = get_courses_licenses(user, courses) +License = namedtuple('License', 'software serial') - for software, license in user_licenses.iteritems(): + +def get_licenses_by_course(user, courses): + licenses = get_courses_licenses(user, courses) + licenses_by_course = defaultdict(list) + + # create missing licenses and group by course_id + for software, license in licenses.iteritems(): if license is None: - user_licenses[software] = get_or_create_user_license(user, software) + licenses[software] = get_or_create_license(user, software) - log.info(user_licenses) + course_id = software.course_id + serial = license.serial if license else None + licenses_by_course[course_id].append(License(software, serial)) - return user_licenses + # render elements + data_by_course = {} + for course_id, licenses in licenses_by_course.iteritems(): + context = {'licenses': licenses} + template = 'licenses/serial_numbers.html' + data_by_course[course_id] = render_to_string(template, context) - -def get_courses_licenses(user, courses): - course_ids = set(course.id for course in courses) - all_software = CourseSoftware.objects.filter(course_id__in=course_ids) - - user_licenses = dict.fromkeys(all_software, None) - - assigned_licenses = UserLicense.objects.filter(software__in=all_software, user=user) - assigned_by_software = {lic.software:lic for lic in assigned_licenses} - - for software, license in assigned_by_software.iteritems(): - user_licenses[software] = license - - return user_licenses - - -def get_or_create_user_license(user, software): - license = None - try: - # Find a licenses associated with the user or with no user - # associated. - query = (Q(user__isnull=True) | Q(user=user)) & Q(software=software) - - # TODO fix a race condition in this code when more than one - # user is getting a license assigned - - license = UserLicense.objects.filter(query)[0] - - if license.user is not user: - license.user = user - license.save() - - except IndexError: - # TODO look if someone has unenrolled from the class and already has a serial number - log.error('No serial numbers available for {0}', software) - - - return license + return data_by_course diff --git a/lms/templates/licenses/serial_numbers.html b/lms/templates/licenses/serial_numbers.html new file mode 100644 index 0000000000..18f0ff8a9b --- /dev/null +++ b/lms/templates/licenses/serial_numbers.html @@ -0,0 +1,10 @@ +
    +% for license in licenses: +
    ${license.software.name}:
    + % if license.serial: +
    ${license.serial}
    + % else: +
    None Available
    + % endif +% endfor +
    From 9cf8c02dc65615e4750b1181a60f8e1dbd5fd4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 22 Aug 2012 14:23:36 -0400 Subject: [PATCH 121/143] Added import_serial_numbers command test. --- lms/djangoapps/licenses/tests.py | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 lms/djangoapps/licenses/tests.py diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py new file mode 100644 index 0000000000..f06899d2de --- /dev/null +++ b/lms/djangoapps/licenses/tests.py @@ -0,0 +1,85 @@ +import logging +from uuid import uuid4 +from random import shuffle +from tempfile import NamedTemporaryFile + +from django.test import TestCase +from django.core.management import call_command + +from models import CourseSoftware, UserLicense + +COURSE_1 = 'MITx/6.002x/2012_Fall' + +SOFTWARE_1 = 'matlab' +SOFTWARE_2 = 'stata' + +log = logging.getLogger(__name__) + + +class CommandTest(TestCase): + def test_import_serial_numbers(self): + size = 20 + + log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1)) + with generate_serials_file(size) as temp_file: + args = [COURSE_1, SOFTWARE_1, temp_file.name] + call_command('import_serial_numbers', *args) + + log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2)) + with generate_serials_file(size) as temp_file: + args = [COURSE_1, SOFTWARE_2, temp_file.name] + call_command('import_serial_numbers', *args) + + log.debug('There should be only 2 course-software entries') + software_count = CourseSoftware.objects.all().count() + self.assertEqual(2, software_count) + + log.debug('We added two sets of {0} serials'.format(size)) + licenses_count = UserLicense.objects.all().count() + self.assertEqual(2 * size, licenses_count) + + log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1)) + with generate_serials_file(size) as temp_file: + args = [COURSE_1, SOFTWARE_1, temp_file.name] + call_command('import_serial_numbers', *args) + + log.debug('There should be still only 2 course-software entries') + software_count = CourseSoftware.objects.all().count() + self.assertEqual(2, software_count) + + log.debug('Now we should have 3 sets of 20 serials'.format(size)) + licenses_count = UserLicense.objects.all().count() + self.assertEqual(3 * size, licenses_count) + + cs = CourseSoftware.objects.get(pk=1) + + lics = UserLicense.objects.filter(software=cs)[:size] + known_serials = list(l.serial for l in lics) + known_serials.extend(generate_serials(10)) + + shuffle(known_serials) + + log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1)) + with NamedTemporaryFile() as f: + f.write('\n'.join(known_serials)) + f.flush() + args = [COURSE_1, SOFTWARE_1, f.name] + call_command('import_serial_numbers', *args) + + log.debug('Check if we added only the new ones') + licenses_count = UserLicense.objects.filter(software=cs).count() + self.assertEqual((2 * size) + 10, licenses_count) + + +def generate_serials(size=20): + return [str(uuid4()) for _ in range(size)] + + +def generate_serials_file(size=20): + serials = generate_serials(size) + + temp_file = NamedTemporaryFile() + temp_file.write('\n'.join(serials)) + temp_file.flush() + + return temp_file From 3808ff85a8f867be5ef4bd7359f6a4593f4a9707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 28 Aug 2012 11:46:12 -0400 Subject: [PATCH 122/143] Added django command to generate random serial numbers. --- .../commands/generate_serial_numbers.py | 65 +++++++++++++++++++ .../commands/import_serial_numbers.py | 1 - 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/licenses/management/commands/generate_serial_numbers.py diff --git a/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py new file mode 100644 index 0000000000..7c6b0d310e --- /dev/null +++ b/lms/djangoapps/licenses/management/commands/generate_serial_numbers.py @@ -0,0 +1,65 @@ +import os.path +from uuid import uuid4 +from optparse import make_option + +from django.utils.html import escape +from django.core.management.base import BaseCommand, CommandError + +from xmodule.modulestore.django import modulestore + +from licenses.models import CourseSoftware, UserLicense + + +class Command(BaseCommand): + help = """Generate random serial numbers for software used in a course. + + Usage: generate_serial_numbers + + is the number of numbers to generate. + + Example: + + import_serial_numbers MITx/6.002x/2012_Fall matlab 100 + + """ + args = "course_id software_id count" + + def handle(self, *args, **options): + """ + """ + course_id, software_name, count = self._parse_arguments(args) + + software, _ = CourseSoftware.objects.get_or_create(course_id=course_id, + name=software_name) + self._generate_serials(software, count) + + def _parse_arguments(self, args): + if len(args) != 3: + raise CommandError("Incorrect number of arguments") + + course_id = args[0] + courses = modulestore().get_courses() + known_course_ids = set(c.id for c in courses) + + if course_id not in known_course_ids: + raise CommandError("Unknown course_id") + + software_name = escape(args[1].lower()) + + try: + count = int(args[2]) + except ValueError: + raise CommandError("Invalid argument.") + + return course_id, software_name, count + + def _generate_serials(self, software, count): + print "Generating {0} serials".format(count) + + # add serial numbers them to the database + for _ in xrange(count): + serial = str(uuid4()) + license = UserLicense(software=software, serial=serial) + license.save() + + print "{0} new serial numbers generated.".format(count) diff --git a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py index 465940ce20..a3a8c0bad1 100644 --- a/lms/djangoapps/licenses/management/commands/import_serial_numbers.py +++ b/lms/djangoapps/licenses/management/commands/import_serial_numbers.py @@ -1,5 +1,4 @@ import os.path - from optparse import make_option from django.utils.html import escape From e9ec63e4b6488ad278edfa98f4566d861da796a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 15 Oct 2012 18:45:36 -0400 Subject: [PATCH 123/143] Add ajax endpoint to retrieve and assign course software licenses --- lms/djangoapps/licenses/models.py | 2 +- lms/djangoapps/licenses/views.py | 53 ++++++++++++++++++++++++++++++- lms/urls.py | 8 +++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index 929fba10ec..d259892f5d 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -65,7 +65,7 @@ def _create_license(user, software): # table/rows with select_for_update to prevent race conditions with transaction.commit_on_success(): selected = UserLicense.objects.select_for_update() - license = selected.filter(user__isnull=True)[0] + license = selected.filter(user__isnull=True, software=software)[0] license.user = user license.save() except IndexError: diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index 7cf8e6591e..9b32478e0e 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -1,9 +1,18 @@ import logging +import json +import re +from urlparse import urlparse from collections import namedtuple, defaultdict + from mitxmako.shortcuts import render_to_string -from models import get_courses_licenses, get_or_create_license +from django.contrib.auth.models import User +from django.http import HttpResponse, Http404 +from django.views.decorators.csrf import requires_csrf_token, csrf_protect + +from models import CourseSoftware +from models import get_courses_licenses, get_or_create_license, get_license log = logging.getLogger("mitx.licenses") @@ -33,3 +42,45 @@ def get_licenses_by_course(user, courses): data_by_course[course_id] = render_to_string(template, context) return data_by_course + + +@requires_csrf_token +def user_software_license(request): + if request.method != 'POST' or not request.is_ajax(): + raise Http404 + + # get the course id from the referer + url_path = urlparse(request.META.get('HTTP_REFERER', '')).path + pattern = re.compile('^/courses/(?P[^/]+/[^/]+/[^/]+)/.*/?$') + match = re.match(pattern, url_path) + + if not match: + raise Http404 + course_id = match.groupdict().get('id', '') + + user_id = request.session.get('_auth_user_id') + software_name = request.POST.get('software') + generate = request.POST.get('generate', False) == 'true' + + print user_id, software_name, generate + + try: + software = CourseSoftware.objects.get(name=software_name, + course_id=course_id) + print software + except CourseSoftware.DoesNotExist: + raise Http404 + + user = User.objects.get(id=user_id) + + if generate: + license = get_or_create_license(user, software) + else: + license = get_license(user, software) + + if license: + response = {'serial': license.serial} + else: + response = {'error': 'No serial number found'} + + return HttpResponse(json.dumps(response), mimetype='application/json') diff --git a/lms/urls.py b/lms/urls.py index 89a541ab06..e025478387 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -154,6 +154,14 @@ if settings.COURSEWARE_ENABLED: url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc', name='preview_chemcalc'), + # Software Licenses + + # TODO: for now, this is the endpoint of an ajax replay + # service that retrieve and assigns license numbers for + # software assigned to a course. The numbers have to be loaded + # into the database. + url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback', name='xqueue_callback'), From a8cedf8ab9712534e8747d62975c7536e8698cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 15 Oct 2012 21:04:10 -0300 Subject: [PATCH 124/143] Update lms/djangoapps/licenses/views.py Remove annoying print statements --- lms/djangoapps/licenses/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/licenses/views.py b/lms/djangoapps/licenses/views.py index 9b32478e0e..7d804fbd3d 100644 --- a/lms/djangoapps/licenses/views.py +++ b/lms/djangoapps/licenses/views.py @@ -62,8 +62,6 @@ def user_software_license(request): software_name = request.POST.get('software') generate = request.POST.get('generate', False) == 'true' - print user_id, software_name, generate - try: software = CourseSoftware.objects.get(name=software_name, course_id=course_id) From e7c62b0fc659e3597e30181091144c0f16792f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 16 Oct 2012 11:57:38 -0400 Subject: [PATCH 125/143] Add migrations to license django application --- .../licenses/migrations/0001_initial.py | 118 ++++++++++++++++++ .../licenses/migrations/__init__.py | 0 2 files changed, 118 insertions(+) create mode 100644 lms/djangoapps/licenses/migrations/0001_initial.py create mode 100644 lms/djangoapps/licenses/migrations/__init__.py diff --git a/lms/djangoapps/licenses/migrations/0001_initial.py b/lms/djangoapps/licenses/migrations/0001_initial.py new file mode 100644 index 0000000000..bdc1d3ead4 --- /dev/null +++ b/lms/djangoapps/licenses/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseSoftware' + db.create_table('licenses_coursesoftware', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('full_name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('url', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('licenses', ['CourseSoftware']) + + # Adding model 'UserLicense' + db.create_table('licenses_userlicense', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('software', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['licenses.CourseSoftware'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)), + ('serial', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('licenses', ['UserLicense']) + + + def backwards(self, orm): + # Deleting model 'CourseSoftware' + db.delete_table('licenses_coursesoftware') + + # Deleting model 'UserLicense' + db.delete_table('licenses_userlicense') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'licenses.coursesoftware': { + 'Meta': {'object_name': 'CourseSoftware'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'licenses.userlicense': { + 'Meta': {'object_name': 'UserLicense'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'serial': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'software': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['licenses.CourseSoftware']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + } + } + + complete_apps = ['licenses'] \ No newline at end of file diff --git a/lms/djangoapps/licenses/migrations/__init__.py b/lms/djangoapps/licenses/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From b479c85310571312fcfe3dd21e6eb8d74f8af915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 16 Oct 2012 13:02:13 -0400 Subject: [PATCH 126/143] Change order of X Universities in the front page --- lms/templates/index.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/templates/index.html b/lms/templates/index.html index b1d9925416..151525f715 100644 --- a/lms/templates/index.html +++ b/lms/templates/index.html @@ -66,14 +66,6 @@
  • - - -
    - UTx -
    -
    -
  • -
  • @@ -81,6 +73,14 @@
  • +
  • + + +
    + UTx +
    +
    +
  • From dbfa7bf03a848b20dce5b0db09be4a1c2f5b4455 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 18 Oct 2012 12:20:52 -0400 Subject: [PATCH 127/143] Change email change success notice to say email instead of name --- lms/templates/email_change_successful.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/email_change_successful.html b/lms/templates/email_change_successful.html index e8c58a282d..0b05a28bf5 100644 --- a/lms/templates/email_change_successful.html +++ b/lms/templates/email_change_successful.html @@ -1,3 +1,3 @@

    E-mail change successful!

    -

    You should see your new name in your dashboard.

    \ No newline at end of file +

    You should see your new email in your dashboard.

    \ No newline at end of file From 3385e063040e81ae04a6e49013b87e5fc7448cfa Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 18 Oct 2012 12:22:44 -0400 Subject: [PATCH 128/143] As per code review, added note that accept_name_change is no longer being used. --- common/djangoapps/student/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index d163f956ac..55bfc0715a 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -743,7 +743,12 @@ def accept_name_change_by_id(id): @ensure_csrf_cookie def accept_name_change(request): - ''' JSON: Name change process. Course staff clicks 'accept' on a given name change ''' + ''' JSON: Name change process. Course staff clicks 'accept' on a given name change + + We used this during the prototype but now we simply record name changes instead + of manually approving them. Still keeping this around in case we want to go + back to this approval method. + ''' if not request.user.is_staff: raise Http404 From 0dee2e9233b25e8673b24b57a43bb580ca7951bd Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 18 Oct 2012 13:33:14 -0400 Subject: [PATCH 129/143] resend the same activation link instead of a new one when someone who is not active tries to log in --- common/djangoapps/student/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 55bfc0715a..bf325a994b 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -559,7 +559,6 @@ def reactivation_email(request): def reactivation_email_for_user(user): reg = Registration.objects.get(user=user) - reg.register(user) d = {'name': user.profile.name, 'key': reg.activation_key} From ea8b9577ceb0c97d3ff3dab653ae9abeeacc185b Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 18 Oct 2012 14:28:15 -0400 Subject: [PATCH 130/143] Email wording changes based on Sarina's review --- lms/templates/emails/confirm_email_change.txt | 3 +-- lms/templates/registration/password_reset_email.html | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/templates/emails/confirm_email_change.txt b/lms/templates/emails/confirm_email_change.txt index e1b5d63376..02aa20facf 100644 --- a/lms/templates/emails/confirm_email_change.txt +++ b/lms/templates/emails/confirm_email_change.txt @@ -1,8 +1,7 @@ <%! from django.core.urlresolvers import reverse %> This is to confirm that you changed the e-mail associated with edX from ${old_email} to ${new_email}. If you did not make this request, -please contact the course staff immediately. Contact information is -listed at: +please contact us immediately. Contact information is listed at: % if is_secure: https://${ site }${reverse('contact')} diff --git a/lms/templates/registration/password_reset_email.html b/lms/templates/registration/password_reset_email.html index 6d906c84ff..bf6c3e0891 100644 --- a/lms/templates/registration/password_reset_email.html +++ b/lms/templates/registration/password_reset_email.html @@ -5,7 +5,8 @@ {% block reset_link %} https://{{domain}}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {% endblock %} -{% trans "Your username, in case you've forgotten:" %} {{ user.username }} + +If you didn't request this change, you can disregard this email - we have not yet reset your password. {% trans "Thanks for using our site!" %} From 9fbdeeb6c68bc7c99e386dd0a80068dec81663ca Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 18 Oct 2012 14:36:52 -0400 Subject: [PATCH 131/143] Send email change notice email to both old and new email addresses. --- common/djangoapps/student/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index bf325a994b..b2fcc73ca3 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -654,9 +654,12 @@ def confirm_email_change(request, key): meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()]) up.set_meta(meta) up.save() + # Send it to the old email... + user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) user.email = pec.new_email user.save() pec.delete() + # And send it to the new email... user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) return render_to_response("email_change_successful.html", d) From 2f7733ede0f74317a890bd5a94ed99f022afd26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Thu, 18 Oct 2012 14:45:42 -0400 Subject: [PATCH 132/143] Add Cengage book press release --- .../images/press/cengage_book_327x400.jpg | Bin 0 -> 31755 bytes .../Cengage_to_provide_book_content.html | 76 ++++++++++++++++++ lms/urls.py | 3 +- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 lms/static/images/press/cengage_book_327x400.jpg create mode 100644 lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html diff --git a/lms/static/images/press/cengage_book_327x400.jpg b/lms/static/images/press/cengage_book_327x400.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e26dc7bf68e177cbe61ec8d80148eba06a8e9916 GIT binary patch literal 31755 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 z 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 TFe3IKit5jnZsk9)m_Ps7?k+gE literal 0 HcmV?d00001 diff --git a/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html b/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html new file mode 100644 index 0000000000..b9d4e95301 --- /dev/null +++ b/lms/templates/static_templates/press_releases/Cengage_to_provide_book_content.html @@ -0,0 +1,76 @@ +<%! from django.core.urlresolvers import reverse %> +<%inherit file="../../main.html" /> + +<%namespace name='static' file='../../static_content.html'/> + +<%block name="title">Cengage Learning To Provide Book Content +
    + + +
    +
    +

    Cengage Learning to Provide Book Content and Pedagogy through edX's Not-for-Profit Interactive Study Via the Web

    +
    + +
    +

    Students taking HarvardX's “PHW207x: Health in Numbers” on edX

    +

    to get access to Principles of Biostatistics, 2nd Edition

    + +

    CAMBRIDGE, MA — October 17, 2012 — EdX, the not-for-profit online learning venture founded by Harvard University and the Massachusetts Institute of Technology (MIT), today announced its collaboration with Cengage Learning, a leading educational content, software and services company, to provide licensed content to edX students at no cost.

    + +

    Students who enroll in edX's course PHW207x: Health in Numbers , taught by Professor Marcello Pagano of Harvard's School of Public Health, will have access to an online version of the course textbook, Principles of Biostatistics, 2nd Edition, written by Marcello Pagano and Kimberlee Gauvreau and published by Cengage Learning. Cengage Learning’s instructional design services will also work with edX to migrate the print pedagogy from the textbook into the on-line course, creating the best scope and sequence for effective student learning.

    + +
    + +
    + +

    “edX students worldwide will benefit from both Professor Pagano's in-class lectures and his classic Cengage Learning textbook in biostatics,” said Anant Agarwal, President of edX. “We are very grateful for Cengage's commitment to helping edX learners throughout the world.”

    + +

     “We're pleased to collaborate with edX and its mission to improve worldwide access to higher education,” said William Rieders, Executive Vice President, Global Strategy & Business Development for Cengage Learning. “Through this collaboration, edX and Cengage Learning are able to bring the best content, services, and delivery to students worldwide.”

    + +

    PHW207x: Health in Numbers, which began on October 15th, is the online adaptation of material from the Harvard School of Public Health's classes in epidemiology and biostatistics. Taught by Professor Pagano and Earl Francis Cook, Professor of Epidemiology at the Harvard School of Public Health (HSPH) and at the Harvard Medical School, PHW207x teaches students the principles of biostatistics and epidemiology used for public health and clinical research.

    + +

    Through edX, the “X Universities” — which now includes UC Berkeley and The University of Texas System in addition to founding institutions Harvard and MIT — will provide interactive education wherever there is access to the Internet and will enhance teaching and learning through research about how students learn, and how technologies can facilitate effective teaching both on-campus and online. EdX plans to add other “X Universities” from around the world to the edX platform in the coming months.

    + +

    About edX

    + +

    edX is a not-for-profit enterprise of its founding partners Harvard University and the Massachusetts Institute of Technology that features learning designed specifically for interactive study via the web. Based on a long history of collaboration and their shared educational missions the founders are creating a new online-learning experience. Anant Agarwal, former Director of MIT's Computer Science and Artificial Intelligence Laboratory, serves as the first president of edX. Along with offering online courses, the institutions will use edX to research how students learn and how technology can transform learning—both on-campus and worldwide. EdX is based in Cambridge, Massachusetts and is governed by MIT and Harvard.

    + +

    About Cengage Learning

    + +

    Cengage Learning is a leading provider of innovative teaching, learning and research solutions for the academic, professional and library markets worldwide. The company's products and services are designed to foster academic excellence and professional development, increase student engagement, improve learning outcomes and deliver authoritative information to people whenever and wherever they need it. Through the company's unique position within both the library and academic markets, Cengage Learning is providing integrated learning solutions that bridge from the library to the classroom. Cengage Learning’s brands include Brooks/Cole, Course Technology, Delmar, Gale, Heinle, National Geographic Learning, South-Western and Wadsworth, among others. Cengage Learning is headquartered in Stamford, CT. For more information on Cengage Learning please visit www.cengage.com

    + +
    +

    Contact: Dan O’Connell

    +

    P: 617-480-6585;

    +

    E: oconnell@edx.org

    +
    + + +
    +
    +
    diff --git a/lms/urls.py b/lms/urls.py index e025478387..db774b8cee 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -91,7 +91,8 @@ urlpatterns = ('', {'template': 'press_releases/Elsevier_collaborates_with_edX.html'}, name="press/elsevier-collaborates-with-edx"), url(r'^press/ut-joins-edx$', 'static_template_view.views.render', {'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"), - + url(r'^press/cengage-to-provide-book-content$', 'static_template_view.views.render', + {'template': 'press_releases/Cengage_to_provide_book_content.html'}, name="press/cengage-to-provide-book-content"), # Should this always update to point to the latest press release? (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), From 5177e8c656480fcf12c22cc697c629e2c5ebd3f0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 18 Oct 2012 20:23:49 -0400 Subject: [PATCH 133/143] Catch unicode errors in contextualize_text and deal with them. --- common/lib/capa/capa/util.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 75bd9fb5bc..68cc23655a 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -28,7 +28,17 @@ def contextualize_text(text, context): # private Does a substitution of those variables from the context ''' if not text: return text for key in sorted(context, lambda x, y: cmp(len(y), len(x))): - text = text.replace('$' + key, str(context[key])) + # TODO (vshnayder): This whole replacement thing is a big hack + # right now--context contains not just just the vars defined + # in the program, but also e.g. a reference to the numpy + # module. Should be a separate dict of variables that are + # should be replaced. + if '$' + key in text: + try: + s = str(context[key]) + except UnicodeEncodeError: + s = context[key].encode('utf8', errors='ignore') + text = text.replace('$' + key, s) return text From 1806699f26f6c1ca7965dee0a83ff897eba293f8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 18 Oct 2012 20:25:52 -0400 Subject: [PATCH 134/143] typos --- common/lib/capa/capa/util.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 68cc23655a..10e984611b 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -26,13 +26,14 @@ def compare_with_tolerance(v1, v2, tol): def contextualize_text(text, context): # private ''' Takes a string with variables. E.g. $a+$b. Does a substitution of those variables from the context ''' - if not text: return text + if not text: + return text for key in sorted(context, lambda x, y: cmp(len(y), len(x))): # TODO (vshnayder): This whole replacement thing is a big hack - # right now--context contains not just just the vars defined - # in the program, but also e.g. a reference to the numpy - # module. Should be a separate dict of variables that are - # should be replaced. + # right now--context contains not just the vars defined in the + # program, but also e.g. a reference to the numpy module. + # Should be a separate dict of variables that should be + # replaced. if '$' + key in text: try: s = str(context[key]) From f890c1dccb9680bef9f8805a3211766d86802fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Fri, 19 Oct 2012 17:55:55 -0400 Subject: [PATCH 135/143] Use test database in licenses test instead of 6.002x --- lms/djangoapps/licenses/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/licenses/tests.py b/lms/djangoapps/licenses/tests.py index f06899d2de..9f4e0e3e4f 100644 --- a/lms/djangoapps/licenses/tests.py +++ b/lms/djangoapps/licenses/tests.py @@ -8,7 +8,7 @@ from django.core.management import call_command from models import CourseSoftware, UserLicense -COURSE_1 = 'MITx/6.002x/2012_Fall' +COURSE_1 = 'edX/toy/2012_Fall' SOFTWARE_1 = 'matlab' SOFTWARE_2 = 'stata' From 3784c3ce18be0705596c666683a3d53a2e971cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Mon, 22 Oct 2012 10:25:49 -0400 Subject: [PATCH 136/143] Add fix for users with multiple licenses --- lms/djangoapps/licenses/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/licenses/models.py b/lms/djangoapps/licenses/models.py index d259892f5d..06f777f611 100644 --- a/lms/djangoapps/licenses/models.py +++ b/lms/djangoapps/licenses/models.py @@ -42,7 +42,10 @@ def get_courses_licenses(user, courses): def get_license(user, software): try: - license = UserLicense.objects.get(user=user, software=software) + # TODO: temporary fix for when somehow a user got more that one license. + # The proper fix should use Meta.unique_together in the UserLicense model. + licenses = UserLicense.objects.filter(user=user, software=software) + license = licenses[0] if licenses else None except UserLicense.DoesNotExist: license = None From 1530341d5551944dc65515a4d4dad0d8420fc11c Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 22 Oct 2012 16:01:57 -0400 Subject: [PATCH 137/143] have import be a bit more flexible in terms of the packaging of the content. Allow for tar.gz names to be different from the course name. Also don't assume that the unpacking of the zip will yield a single top level root folder. --- cms/djangoapps/contentstore/utils.py | 2 +- cms/djangoapps/contentstore/views.py | 45 ++++++++++++++----- .../xmodule/modulestore/xml_importer.py | 4 +- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 582123b78f..18afd331d0 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -27,7 +27,7 @@ def get_course_location_for_item(location): raise BaseException('Could not find course at {0}'.format(course_search_location)) if found_cnt > 1: - raise BaseException('Found more than one course at {0}. There should only be one!!!'.format(course_search_location)) + raise BaseException('Found more than one course at {0}. There should only be one!!! Dump = {1}'.format(course_search_location, courses)) location = courses[0].location diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index adf79ffe41..eb635bc30d 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -10,6 +10,7 @@ import sys import time import tarfile import shutil +import tempfile from datetime import datetime from collections import defaultdict from uuid import uuid4 @@ -943,6 +944,12 @@ def create_new_course(request): if existing_course is not None: return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with this name.'})) + course_search_location = ['i4x', dest_location.org, dest_location.course, 'course', None] + courses = modulestore().get_items(course_search_location) + + if len(courses) > 0: + return HttpResponse(json.dumps({'ErrMsg': 'There is already a course defined with the same organization and course number.'})) + new_course = modulestore('direct').clone_item(template, dest_location) if display_name is not None: @@ -978,7 +985,11 @@ def import_course(request, org, course, name): data_root = path(settings.GITHUB_REPO_ROOT) - temp_filepath = data_root / filename + course_dir = data_root / "{0}-{1}-{2}".format(org, course, name) + if not course_dir.isdir(): + os.mkdir(course_dir) + + temp_filepath = course_dir / filename logging.debug('importing course to {0}'.format(temp_filepath)) @@ -988,32 +999,42 @@ def import_course(request, org, course, name): temp_file.write(chunk) temp_file.close() - # @todo: don't assume the top-level directory that was unziped was the same name (but without .tar.gz) - course_dir = filename.replace('.tar.gz', '') - tf = tarfile.open(temp_filepath) - if (data_root / course_dir).isdir(): - shutil.rmtree(data_root / course_dir) - tf.extractall(data_root + '/') + tf.extractall(course_dir + '/') - os.remove(temp_filepath) # remove the .tar.gz file + # find the 'course.xml' file + for r,d,f in os.walk(course_dir): + for files in f: + if files == 'course.xml': + break + if files == 'course.xml': + break - with open(data_root / course_dir / 'course.xml', 'r') as course_file: + if files != 'course.xml': + return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'})) + + logging.debug('found course.xml at {0}'.format(r)) + + if r != course_dir: + for fname in os.listdir(r): + shutil.move(r/fname, course_dir) + + with open(course_dir / 'course.xml', 'r') as course_file: course_data = etree.parse(course_file, parser=edx_xml_parser) course_data_root = course_data.getroot() course_data_root.set('org', org) course_data_root.set('course', course) course_data_root.set('url_name', name) - with open(data_root / course_dir / 'course.xml', 'w') as course_file: + with open(course_dir / 'course.xml', 'w') as course_file: course_data.write(course_file) module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, [course_dir], load_error_modules=False, static_content_store=contentstore()) - # remove content directory - we *shouldn't* need this any longer :-) - shutil.rmtree(data_root / course_dir) + # we can blow this away when we're done importing. + shutil.rmtree(course_dir) logging.debug('new course at {0}'.format(course_items[0].location)) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 37e57bbcd5..9e03851b88 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -29,7 +29,9 @@ def import_static_content(modules, data_dir, static_content_store): # now import all static assets - static_dir = '{0}/{1}/static/'.format(data_dir, course_data_dir) + static_dir = '{0}/static/'.format(course_data_dir) + + logging.debug("Importing static assets in {0}".format(static_dir)) for dirname, dirnames, filenames in os.walk(static_dir): for filename in filenames: From df0ca3c445ce78f220cfa4905212f1ab24f1528b Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 22 Oct 2012 16:09:13 -0400 Subject: [PATCH 138/143] changed save button to add user --- cms/templates/manage_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/manage_users.html b/cms/templates/manage_users.html index 7a1a35c58e..618e3455f7 100644 --- a/cms/templates/manage_users.html +++ b/cms/templates/manage_users.html @@ -21,7 +21,7 @@ %if allow_actions:
    - save + add user cancel
    %endif From 90a55714c2a054e26b77fee4eae3308504e6a10a Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 22 Oct 2012 16:35:22 -0400 Subject: [PATCH 139/143] trim section name strings to remove whitespace --- cms/static/js/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 5e91eca875..3279b337ee 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -686,7 +686,7 @@ function saveEditSectionName(e) { e.preventDefault(); id = $(this).closest("section.courseware-section").data("id"); - display_name = $(this).prev('.edit-section-name').val(); + display_name = $.trim($(this).prev('.edit-section-name').val()); if (display_name == '') { alert("You must specify a name before saving.") From e3457fa230b65524b71a53139843657fc0b346b9 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Thu, 18 Oct 2012 10:40:24 -0400 Subject: [PATCH 140/143] started styling section date pickers --- cms/static/js/base.js | 2 +- cms/static/sass/_courseware.scss | 51 +++++++++++++++++++++++++------- cms/templates/overview.html | 21 +++++-------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 5e91eca875..8834d69dc8 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -734,7 +734,7 @@ function saveSetSectionScheduleDate(e) { id = $(this).closest("section.courseware-section").data("id"); var $_this = $(this); - // call into server to commit the new order + // call into server to commit the new order $.ajax({ url: "/save_item", type: "POST", diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss index 752c33346f..0ba4b84226 100644 --- a/cms/static/sass/_courseware.scss +++ b/cms/static/sass/_courseware.scss @@ -28,14 +28,45 @@ input.courseware-unit-search-input { &.collapsed { padding-bottom: 0; + } - header { - height: 47px; - } - - h4 { - display: none !important; - } + label { + float: left; + line-height: 29px; + } + + .datepair { + float: left; + margin-left: 10px; + } + + .section-published-date { + position: absolute; + top: 12px; + right: 90px; + width: 250px; + font-size: 13px; + } + + .datepair .date, + .datepair .time { + padding-left: 0; + padding-right: 0; + border: none; + background: none; + @include box-shadow(none); + font-size: 13px; + font-weight: 700; + color: $blue; + cursor: pointer; + } + + .datepair .date { + width: 80px; + } + + .datepair .time { + width: 65px; } &.collapsed .subsection-list, @@ -45,11 +76,11 @@ input.courseware-unit-search-input { } header { - height: 67px; + height: 55px; .item-details { float: left; - padding: 10px 0 0; + padding: 15px 0 0; } .item-actions { @@ -64,7 +95,7 @@ input.courseware-unit-search-input { .expand-collapse-icon { float: left; - margin: 16px 6px 16px 16px; + margin: 20px 6px 16px 16px; @include transition(none); } diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 17283fe4b2..91b39d7586 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -74,24 +74,19 @@ SaveCancel - - +
    + + + +
    +
    From f84b603133cdad1fc31d7b434b02c38572670b52 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 22 Oct 2012 13:10:52 -0400 Subject: [PATCH 141/143] wrapped up section date setting; need help with already published sections --- cms/static/js/base.js | 61 ++++++++++-- cms/static/sass/_courseware.scss | 95 +++++++++++++++++-- cms/templates/overview.html | 29 ++++-- .../vendor/timepicker/jquery.timepicker.css | 2 +- 4 files changed, 162 insertions(+), 25 deletions(-) diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 8834d69dc8..4f162127be 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -10,7 +10,8 @@ var $spinner; $(document).ready(function() { $body = $('body'); $modal = $('.history-modal'); - $modalCover = $('.modal-cover'); + $modalCover = $('
    diff --git a/common/static/js/vendor/timepicker/jquery.timepicker.css b/common/static/js/vendor/timepicker/jquery.timepicker.css index 045751cc06..ad6dae98b8 100755 --- a/common/static/js/vendor/timepicker/jquery.timepicker.css +++ b/common/static/js/vendor/timepicker/jquery.timepicker.css @@ -11,7 +11,7 @@ -moz-box-shadow: 0 5px 10px rgba(0,0,0,0.1); box-shadow: 0 5px 10px rgba(0,0,0,0.1); outline: none; - z-index: 10001; + z-index: 100001; font-size: 12px; } From 1d40fa33600d681d9a9fadecef28c734405df1f0 Mon Sep 17 00:00:00 2001 From: Tom Giannattasio Date: Mon, 22 Oct 2012 13:21:45 -0400 Subject: [PATCH 142/143] tweaked styles for section naming --- cms/static/sass/_courseware.scss | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cms/static/sass/_courseware.scss b/cms/static/sass/_courseware.scss index e064428dee..094b6183dd 100644 --- a/cms/static/sass/_courseware.scss +++ b/cms/static/sass/_courseware.scss @@ -135,6 +135,32 @@ input.courseware-unit-search-input { color: $blue; } + .section-name-span { + cursor: pointer; + @include transition(color .15s); + + &:hover { + color: $orange; + } + } + + .section-name-edit { + input { + font-size: 16px; + } + + .save-button { + @include blue-button; + padding: 7px 20px 7px; + margin-right: 5px; + } + + .cancel-button { + @include white-button; + padding: 7px 20px 7px; + } + } + h4 { font-size: 12px; color: #878e9d; From ef1ba6d9f7cbd58098805aa577d500628e6e185e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 23 Oct 2012 13:40:34 -0400 Subject: [PATCH 143/143] Remove leftover merge garbage --- lms/envs/common.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 9948a33ed5..3330bf0ce3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -338,12 +338,6 @@ TEMPLATE_LOADERS = ( # 'django.template.loaders.filesystem.Loader', # 'django.template.loaders.app_directories.Loader', -<<<<<<< HEAD - -======= - - #'askbot.skins.loaders.filesystem_load_template_source', ->>>>>>> origin/master # 'django.template.loaders.eggs.Loader', )