');
this.assignmentGrade = new CMS.Models.AssignmentGrade({
- assignmentUrl : this.$el.closest('.id-holder').data('id'),
+ assignmentUrl : this.$el.closest('.id-holder').data('id'),
graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null
this.graders = this.options['graders'];
@@ -78,13 +78,13 @@ CMS.Views.OverviewAssignmentGrader = Backbone.View.extend({
},
selectGradeType : function(e) {
e.preventDefault();
-
+
this.removeMenu(e);
// TODO I'm not happy with this string fetch via the html for what should be an id. I'd rather use the id attr
// of the CourseGradingPolicy model or null for Not Graded (NOTE, change template's if check for is-selected accordingly)
this.assignmentGrade.save('graderType', $(e.target).text());
-
+
this.render();
}
-})
\ No newline at end of file
+})
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index 7d92ab69ad..42ed2d6920 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -6,26 +6,26 @@ $(document).ready(function() {
$('.unit').draggable({
axis: 'y',
handle: '.drag-handle',
- zIndex: 999,
+ zIndex: 999,
start: initiateHesitate,
// left 2nd arg in as inert selector b/c i was uncertain whether we'd try to get the shove up/down
// to work in the future
- drag: generateCheckHoverState('.collapsed', ''),
+ drag: generateCheckHoverState('.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
-
+
// Subsection reordering
$('.id-holder').draggable({
axis: 'y',
handle: '.section-item .drag-handle',
- zIndex: 999,
+ zIndex: 999,
start: initiateHesitate,
drag: generateCheckHoverState('.courseware-section.collapsed', ''),
stop: removeHesitate,
revert: "invalid"
});
-
+
// Section reordering
$('.courseware-section').draggable({
axis: 'y',
@@ -33,8 +33,8 @@ $(document).ready(function() {
stack: '.courseware-section',
revert: "invalid"
});
-
-
+
+
$('.sortable-unit-list').droppable({
accept : '.unit',
greedy: true,
@@ -50,7 +50,7 @@ $(document).ready(function() {
drop: onSubsectionReordered,
greedy: true
});
-
+
// Section reordering
$('.courseware-overview').droppable({
accept : '.courseware-section',
@@ -58,7 +58,7 @@ $(document).ready(function() {
drop: onSectionReordered,
greedy: true
});
-
+
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
@@ -87,7 +87,7 @@ function computeIntersection(droppable, uiHelper, y) {
$.extend(droppable, {offset : $(droppable).offset()});
- var t = droppable.offset.top,
+ var t = droppable.offset.top,
b = t + droppable.proportions.height;
if (t === b) {
@@ -118,10 +118,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
-
+
$(selectorsToShove).each(function() {
var intersectsBottom = computeIntersection(this, ui.helper, (draggable.positionAbs || draggable.position.absolute).top);
-
+
if ($(this).hasClass('ui-dragging-pushup')) {
if (!intersectsBottom) {
console.log('not up', $(this).data('id'));
@@ -132,10 +132,10 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('up', $(this).data('id'));
$(this).addClass('ui-dragging-pushup');
}
-
- var intersectsTop = computeIntersection(this, ui.helper,
+
+ var intersectsTop = computeIntersection(this, ui.helper,
(draggable.positionAbs || draggable.position.absolute).top + draggable.helperProportions.height);
-
+
if ($(this).hasClass('ui-dragging-pushdown')) {
if (!intersectsTop) {
console.log('not down', $(this).data('id'));
@@ -146,7 +146,7 @@ function generateCheckHoverState(selectorsToOpen, selectorsToShove) {
console.log('down', $(this).data('id'));
$(this).addClass('ui-dragging-pushdown');
}
-
+
});
}
}
@@ -159,20 +159,20 @@ function removeHesitate(event, ui) {
}
function expandSection(event) {
- $(event.delegateTarget).removeClass('collapsed', 400);
+ $(event.delegateTarget).removeClass('collapsed', 400);
// don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
}
function onUnitReordered(event, ui) {
// a unit's been dropped on this subsection,
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'subsection-id', 'li:.leaf');
}
function onSubsectionReordered(event, ui) {
// a subsection has been dropped on this section,
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
_handleReorder(event, ui, 'section-id', 'li:.branch');
}
@@ -182,7 +182,7 @@ function onSectionReordered(event, ui) {
}
function _handleReorder(event, ui, parentIdField, childrenSelector) {
- // figure out where it came from and where it slots in.
+ // figure out where it came from and where it slots in.
var subsection_id = $(event.target).data(parentIdField);
var _els = $(event.target).children(childrenSelector);
var children = _els.map(function(idx, el) { return $(el).data('id'); }).get();
diff --git a/cms/static/js/views/server_error.js b/cms/static/js/views/server_error.js
index 11478b5712..c97a812358 100644
--- a/cms/static/js/views/server_error.js
+++ b/cms/static/js/views/server_error.js
@@ -1,4 +1,4 @@
CMS.ServerError = function(model, error) {
// this handler is for the client:server communication not the validation errors which handleValidationError catches
window.alert("Server Error: " + error.responseText);
-};
\ No newline at end of file
+};
diff --git a/cms/static/js/views/validating_view.js b/cms/static/js/views/validating_view.js
index 3376e5fe9b..5635d1e357 100644
--- a/cms/static/js/views/validating_view.js
+++ b/cms/static/js/views/validating_view.js
@@ -1,6 +1,6 @@
CMS.Views.ValidatingView = Backbone.View.extend({
- // Intended as an abstract class which catches validation errors on the model and
- // decorates the fields. Needs wiring per class, but this initialization shows how
+ // Intended as an abstract class which catches validation errors on the model and
+ // decorates the fields. Needs wiring per class, but this initialization shows how
// either have your init call this one or copy the contents
initialize : function() {
this.listenTo(this.model, 'error', CMS.ServerError);
@@ -15,7 +15,7 @@ CMS.Views.ValidatingView = Backbone.View.extend({
"change textarea" : "clearValidationErrors"
},
fieldToSelectorMap : {
- // Your subclass must populate this w/ all of the model keys and dom selectors
+ // Your subclass must populate this w/ all of the model keys and dom selectors
// which may be the subjects of validation errors
},
_cacheValidationErrors : [],
From 5c55595e8bae9f77731e82a5063f1612895d90d8 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 12:10:49 -0400
Subject: [PATCH 012/123] Start to add in some more open ended module tests
---
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../xmodule/tests/test_combined_open_ended.py | 63 ++++++++++++++++++-
common/test/data/open_ended/README.md | 1 +
common/test/data/open_ended/course.xml | 1 +
.../test/data/open_ended/course/2012_Fall.xml | 5 ++
.../data/open_ended/policies/2012_Fall.json | 14 +++++
.../test/data/open_ended/roots/2012_Fall.xml | 1 +
.../selfassessment/SampleQuestion.xml | 26 ++++----
8 files changed, 98 insertions(+), 15 deletions(-)
create mode 100644 common/test/data/open_ended/README.md
create mode 100644 common/test/data/open_ended/course.xml
create mode 100644 common/test/data/open_ended/course/2012_Fall.xml
create mode 100644 common/test/data/open_ended/policies/2012_Fall.json
create mode 100644 common/test/data/open_ended/roots/2012_Fall.xml
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 1a10654f6c..59495048a1 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem
from mock import Mock
open_ended_grading_interface = {
- 'url': 'http://sandbox-grader-001.m.edx.org/peer_grading',
+ 'url': 'blah',
'username': 'incorrect_user',
'password': 'incorrect_pass',
'staff_grading' : 'staff_grading',
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 59f0e222ee..8e9e63b530 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,12 +2,19 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
+from fs.memoryfs import MemoryFS
+from mock import patch
+
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
-
+from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+
+from xmodule.tests.test_export import DATA_DIR
+
from lxml import etree
import capa.xqueue_interface as xqueue_interface
from datetime import datetime
@@ -17,8 +24,36 @@ log = logging.getLogger(__name__)
from . import test_system
+ORG = 'test_org'
+COURSE = 'open_ended' # name of directory with course data
+
import test_util_open_ended
+class DummySystem(ImportSystem):
+
+ @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
+ def __init__(self, load_error_modules):
+
+ xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
+ course_id = "/".join([ORG, COURSE, 'test_run'])
+ course_dir = "test_dir"
+ policy = {}
+ error_tracker = Mock()
+ parent_tracker = Mock()
+
+ super(DummySystem, self).__init__(
+ xmlstore,
+ course_id,
+ course_dir,
+ policy,
+ error_tracker,
+ parent_tracker,
+ load_error_modules=load_error_modules,
+ )
+
+ def render_template(self, template, context):
+ raise Exception("Shouldn't be called")
+
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -436,6 +471,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = combinedoe.update_task_states()
self.assertFalse(changed)
+ def test_ajax_actions(self):
+ self.combinedoe_container.handle_ajax('save_answer', {'student_answer' : "This is my answer"})
+
def test_get_score_realistic(self):
instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "is_graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
instance_state = json.loads(instance_state)
@@ -466,6 +504,29 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
+class OpenEndedModuleXmlTest(unittest.TestCase):
+ def setUp(self):
+ self.test_system = test_system()
+
+ @staticmethod
+ def get_import_system(load_error_modules=True):
+ '''Get a dummy system'''
+ return DummySystem(load_error_modules)
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+ print "Importing {0}".format(name)
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ self.assertEquals(len(courses), 1)
+ return courses[0]
+
+ def test_open_ended_load(self):
+ course = self.get_course('open_ended')
+ log.info(course.id)
+
diff --git a/common/test/data/open_ended/README.md b/common/test/data/open_ended/README.md
new file mode 100644
index 0000000000..7fe58ac17f
--- /dev/null
+++ b/common/test/data/open_ended/README.md
@@ -0,0 +1 @@
+This is a very very simple course, useful for debugging self assessment code.
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
new file mode 100644
index 0000000000..ea7d5c420d
--- /dev/null
+++ b/common/test/data/open_ended/course.xml
@@ -0,0 +1 @@
+
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
new file mode 100644
index 0000000000..f2d16488a7
--- /dev/null
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/common/test/data/open_ended/policies/2012_Fall.json b/common/test/data/open_ended/policies/2012_Fall.json
new file mode 100644
index 0000000000..09b68ab400
--- /dev/null
+++ b/common/test/data/open_ended/policies/2012_Fall.json
@@ -0,0 +1,14 @@
+{
+ "course/2012_Fall": {
+ "graceperiod": "2 days 5 hours 59 minutes 59 seconds",
+ "start": "2015-07-17T12:00",
+ "display_name": "Self Assessment Test",
+ "graded": "true"
+ },
+ "chapter/Overview": {
+ "display_name": "Overview"
+ },
+ "combinedopenended/SampleQuestion": {
+ "display_name": "Sample Question",
+ },
+}
diff --git a/common/test/data/open_ended/roots/2012_Fall.xml b/common/test/data/open_ended/roots/2012_Fall.xml
new file mode 100644
index 0000000000..ea7d5c420d
--- /dev/null
+++ b/common/test/data/open_ended/roots/2012_Fall.xml
@@ -0,0 +1 @@
+
diff --git a/common/test/data/self_assessment/selfassessment/SampleQuestion.xml b/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
index 6c383763b1..f8affa903d 100644
--- a/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
+++ b/common/test/data/self_assessment/selfassessment/SampleQuestion.xml
@@ -1,14 +1,14 @@
-
- What is the meaning of life?
-
-
- This is a rubric.
-
-
- Thanks for your submission!
-
-
- Enter a hint below:
-
-
+
+ What is the meaning of life?
+
+
+ This is a rubric.
+
+
+ Thanks for your submission!
+
+
+ Enter a hint below:
+
+
\ No newline at end of file
From d05ba84f1317820ce7265745b11e99e68c275c0c Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 13:49:42 -0400
Subject: [PATCH 013/123] /usr/bin/env in shebang line
---
i18n/extract.py | 2 +-
i18n/generate.py | 2 +-
i18n/make_dummy.py | 2 +-
i18n/transifex.py | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/i18n/extract.py b/i18n/extract.py
index 9b0ad3829c..2cb4ebe118 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
diff --git a/i18n/generate.py b/i18n/generate.py
index ffc88b64d0..1deb1beeae 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
"""
See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py
index 8d0fb95ef2..6c14edd45a 100755
--- a/i18n/make_dummy.py
+++ b/i18n/make_dummy.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
# Generate test translation files from human-readable po files.
#
diff --git a/i18n/transifex.py b/i18n/transifex.py
index d08a77b1c0..ac203f3eea 100755
--- a/i18n/transifex.py
+++ b/i18n/transifex.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
import os, sys
from polib import pofile
From dc473e6f7b5f919844493e6782a017f56d589194 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:07:57 -0400
Subject: [PATCH 014/123] more verbose messages.po
---
conf/locale/en/LC_MESSAGES/messages.po | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/conf/locale/en/LC_MESSAGES/messages.po b/conf/locale/en/LC_MESSAGES/messages.po
index 1bb8bf6d7f..e5961753c5 100644
--- a/conf/locale/en/LC_MESSAGES/messages.po
+++ b/conf/locale/en/LC_MESSAGES/messages.po
@@ -1 +1,20 @@
+# edX translation file
+# Copyright (C) 2013 edX
+# This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: EdX Studio\n"
+"Report-Msgid-Bugs-To: translation_team@edx.org\n"
+"POT-Creation-Date: 2013-05-02 13:13-0400\n"
+"PO-Revision-Date: 2013-05-02 13:27-0400\n"
+"Last-Translator: \n"
+"Language-Team: translation team \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
# empty
+msgid "This is a key string."
+msgstr ""
From beb4b39b73f84dbd839f9802e1508c3e0abc2fbe Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:25:31 -0400
Subject: [PATCH 015/123] fix logging
---
i18n/extract.py | 6 +++---
i18n/generate.py | 6 +++---
i18n/logger.py | 13 -------------
i18n/tests/test_validate.py | 8 ++++----
4 files changed, 10 insertions(+), 23 deletions(-)
delete mode 100644 i18n/logger.py
diff --git a/i18n/extract.py b/i18n/extract.py
index 2cb4ebe118..c517de3b51 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -15,11 +15,10 @@ See https://edx-wiki.atlassian.net/wiki/display/ENG/PO+File+workflow
"""
-import os
+import os, sys, logging
from datetime import datetime
from polib import pofile
-from logger import get_logger
from config import BASE_DIR, LOCALE_DIR, CONFIGURATION
from execute import execute, create_dir_if_necessary, remove_file
@@ -34,7 +33,8 @@ BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
def main ():
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
create_dir_if_necessary(LOCALE_DIR)
source_msgs_dir = CONFIGURATION.source_messages_dir
diff --git a/i18n/generate.py b/i18n/generate.py
index 1deb1beeae..48470796a2 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -13,10 +13,9 @@
languages to generate.
"""
-import os
+import os, sys, logging
from polib import pofile
-from logger import get_logger
from config import BASE_DIR, CONFIGURATION
from execute import execute, remove_file
@@ -72,7 +71,8 @@ def validate_files(dir, files_to_merge):
raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
for locale in CONFIGURATION.locales:
merge(locale)
diff --git a/i18n/logger.py b/i18n/logger.py
deleted file mode 100644
index 20d767a032..0000000000
--- a/i18n/logger.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import logging
-
-def get_logger(name):
- """
- Returns a default logger.
- logging.basicConfig does not render to the console
- """
- log = logging.getLogger()
- log.setLevel(logging.INFO)
- log_handler = logging.StreamHandler()
- log_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
- log.addHandler(log_handler)
- return log
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 6bb7164a50..67057a30e7 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -1,8 +1,7 @@
-import os
+import os, sys, logging
from unittest import TestCase
from nose.plugins.skip import SkipTest
-from logger import get_logger
from config import LOCALE_DIR
from execute import call
@@ -10,10 +9,11 @@ def test_po_files(root=LOCALE_DIR):
"""
This is a generator. It yields all of the .po files under root, and tests each one.
"""
- log = get_logger(__name__)
+ log = logging.getLogger(__name__)
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO)
+
for (dirpath, dirnames, filenames) in os.walk(root):
for name in filenames:
- print name
(base, ext) = os.path.splitext(name)
if ext.lower() == '.po':
yield validate_po_file, os.path.join(dirpath, name), log
From dfcbb73662ae6d845213ace78ba450c010ad61b7 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 14:55:16 -0400
Subject: [PATCH 016/123] add guard code to ensure gnu gettext utilities are
loaded before rake tests
---
rakefile | 34 ++++++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/rakefile b/rakefile
index acb9bf6ecc..64afb2e389 100644
--- a/rakefile
+++ b/rakefile
@@ -522,19 +522,39 @@ def validate_transifex_config()
return true
end
+# Make sure GNU gettext utilities are available
+# Returns boolean: returns true if utilities are available, else returns false
+def validate_gnu_gettext()
+ begin
+ select_executable('xgettext')
+ return true
+ rescue
+ puts "Error:".red
+ puts "Cannot locate GNU gettext utilities, which are required by django for internationalization.".red
+ puts "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)".red
+ puts "Try downloading them from http://www.gnu.org/software/gettext/".red
+ return false
+ end
+end
+
+
namespace :i18n do
desc "Extract localizable strings from sources"
task :extract do
- sh(File.join(REPO_ROOT, "i18n", "extract.py"))
+ if validate_gnu_gettext()
+ sh(File.join(REPO_ROOT, "i18n", "extract.py"))
+ end
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
task :generate do
- if ARGV.last.downcase == 'extract'
- Rake::Task["i18n:extract"].execute
+ if validate_gnu_gettext()
+ if ARGV.last.downcase == 'extract'
+ Rake::Task["i18n:extract"].execute
+ end
+ sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
- sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
desc "Simulate international translation by generating dummy strings corresponding to source strings."
@@ -567,8 +587,10 @@ namespace :i18n do
desc "Run tests for the internationalization library"
task :test do
- test = File.join(REPO_ROOT, "i18n", "tests")
- sh("nosetests #{test}")
+ if validate_gnu_gettext()
+ test = File.join(REPO_ROOT, "i18n", "tests")
+ sh("nosetests #{test}")
+ end
end
end
From 165e7059c81c10005f6412594dc3167eaef13da6 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Wed, 8 May 2013 15:34:35 -0400
Subject: [PATCH 017/123] guard predicates are rake tasks, not functions
---
rakefile | 88 ++++++++++++++++++++++++++------------------------------
1 file changed, 40 insertions(+), 48 deletions(-)
diff --git a/rakefile b/rakefile
index 64afb2e389..3edbc39067 100644
--- a/rakefile
+++ b/rakefile
@@ -510,51 +510,21 @@ end
# --- Internationalization tasks
-# Make sure config file with username/password exists
-# Returns boolean: returns true if file exists and is nonzero length
-def validate_transifex_config()
- config_file = "#{Dir.home}/.transifexrc"
- if !File.file?(config_file) or File.size(config_file)==0
- raise "Cannot connect to Transifex, config file is missing or empty: #{config_file}\n" +
- "See http://help.transifex.com/features/client/#transifexrc\n"
- return false
- end
- return true
-end
-
-# Make sure GNU gettext utilities are available
-# Returns boolean: returns true if utilities are available, else returns false
-def validate_gnu_gettext()
- begin
- select_executable('xgettext')
- return true
- rescue
- puts "Error:".red
- puts "Cannot locate GNU gettext utilities, which are required by django for internationalization.".red
- puts "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)".red
- puts "Try downloading them from http://www.gnu.org/software/gettext/".red
- return false
- end
-end
-
-
namespace :i18n do
desc "Extract localizable strings from sources"
task :extract do
- if validate_gnu_gettext()
- sh(File.join(REPO_ROOT, "i18n", "extract.py"))
- end
+ Rake::Task["i18n:validate:gettext"].execute
+ sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
task :generate do
- if validate_gnu_gettext()
- if ARGV.last.downcase == 'extract'
- Rake::Task["i18n:extract"].execute
- end
- sh(File.join(REPO_ROOT, "i18n", "generate.py"))
+ Rake::Task["i18n:validate:gettext"].execute
+ if ARGV.last.downcase == 'extract'
+ Rake::Task["i18n:extract"].execute
end
+ sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
desc "Simulate international translation by generating dummy strings corresponding to source strings."
@@ -567,30 +537,52 @@ namespace :i18n do
end
end
+ namespace :validate do
+
+ desc "Make sure GNU gettext utilities are available"
+ task :gettext do
+ begin
+ select_executable('xgettext')
+ rescue
+ msg = "Cannot locate GNU gettext utilities, which are required by django for internationalization.\n"
+ msg += "(see https://docs.djangoproject.com/en/dev/topics/i18n/translation/#message-files)\n"
+ msg += "Try downloading them from http://www.gnu.org/software/gettext/"
+ abort(msg.red)
+ end
+ end
+
+ desc "Make sure config file with username/password exists"
+ task :transifex_config do
+ config_file = "#{Dir.home}/.transifexrc"
+ if !File.file?(config_file) or File.size(config_file)==0
+ msg ="Cannot connect to Transifex, config file is missing or empty: #{config_file}\n"
+ msg += "See http://help.transifex.com/features/client/#transifexrc"
+ abort(msg.red)
+ end
+ end
+ end
+
namespace :transifex do
desc "Push source strings to Transifex for translation"
task :push do
- if validate_transifex_config()
- cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
- sh("#{cmd} push")
- end
+ Rake::Task["i18n:validate:transifex_config"].execute
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} push")
end
desc "Pull translated strings from Transifex"
task :pull do
- if validate_transifex_config()
- cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
- sh("#{cmd} pull")
- end
+ Rake::Task["i18n:validate:transifex_config"].execute
+ cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
+ sh("#{cmd} pull")
end
end
desc "Run tests for the internationalization library"
task :test do
- if validate_gnu_gettext()
- test = File.join(REPO_ROOT, "i18n", "tests")
- sh("nosetests #{test}")
- end
+ Rake::Task["i18n:validate:gettext"].execute
+ test = File.join(REPO_ROOT, "i18n", "tests")
+ sh("nosetests #{test}")
end
end
From 1d53625673a33dec59fc9bd2558936517bfec350 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 15:37:25 -0400
Subject: [PATCH 018/123] Add in ability to mock a server, a lot more testing
code for open ended
---
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../lib/xmodule/xmodule/tests/mock_server.py | 34 +++++++++
.../xmodule/tests/test_combined_open_ended.py | 75 +++++++++++++++++--
common/test/data/open_ended/README.md | 2 +-
.../combinedopenended/SampleQuestion.xml | 33 ++++++++
common/test/data/open_ended/course.xml | 2 +-
6 files changed, 140 insertions(+), 8 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/tests/mock_server.py
create mode 100644 common/test/data/open_ended/combinedopenended/SampleQuestion.xml
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 59495048a1..b58308252e 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -20,7 +20,7 @@ from xmodule.x_module import ModuleSystem
from mock import Mock
open_ended_grading_interface = {
- 'url': 'blah',
+ 'url': 'blah/',
'username': 'incorrect_user',
'password': 'incorrect_pass',
'staff_grading' : 'staff_grading',
diff --git a/common/lib/xmodule/xmodule/tests/mock_server.py b/common/lib/xmodule/xmodule/tests/mock_server.py
new file mode 100644
index 0000000000..d8ae2f9124
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/mock_server.py
@@ -0,0 +1,34 @@
+from threading import Thread
+import socket
+import threading
+
+import SimpleHTTPServer
+import SocketServer
+
+class ThreadedRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+ def handle(self):
+ data = self.request.recv(1024)
+ cur_thread = threading.current_thread()
+ response = "{}: {}".format(cur_thread.name, data)
+ self.request.sendall(response)
+ return
+
+class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ pass
+
+def create_server(host,port):
+ """
+ Mock a server to be used for the open ended grading tests
+ @param host: the hostname ie "localhost" or "127.0.0.1"
+ @param port: the integer of the port to open a connection on
+ @return: The created server object
+ """
+ server = ThreadedTCPServer((host,port), ThreadedRequestHandler)
+
+ # Start a thread with the server -- that thread will then start one
+ # more thread for each request
+ server_thread = threading.Thread(target=server.serve_forever)
+ # Exit the server thread when the main thread terminates
+ server_thread.daemon = True
+ server_thread.start()
+ return server
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 8e9e63b530..0f7db73db4 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -20,6 +20,8 @@ import capa.xqueue_interface as xqueue_interface
from datetime import datetime
import logging
+from mock_server import create_server
+
log = logging.getLogger(__name__)
from . import test_system
@@ -29,6 +31,18 @@ COURSE = 'open_ended' # name of directory with course data
import test_util_open_ended
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
+
class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
@@ -471,9 +485,6 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = combinedoe.update_task_states()
self.assertFalse(changed)
- def test_ajax_actions(self):
- self.combinedoe_container.handle_ajax('save_answer', {'student_answer' : "This is my answer"})
-
def test_get_score_realistic(self):
instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "is_graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
instance_state = json.loads(instance_state)
@@ -505,6 +516,11 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['total'], 15.0)
class OpenEndedModuleXmlTest(unittest.TestCase):
+ problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
+ answer = "blah blah"
+ assessment = [0,1]
+ hint = "blah"
+ test_server = create_server("127.0.0.1", 3034)
def setUp(self):
self.test_system = test_system()
@@ -523,10 +539,59 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
self.assertEquals(len(courses), 1)
return courses[0]
- def test_open_ended_load(self):
+ def get_module_from_location(self, location):
course = self.get_course('open_ended')
- log.info(course.id)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
+ def test_open_ended_load_and_save(self):
+ module = self.get_module_from_location(self.problem_location)
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+ def test_open_ended_flow_reset(self):
+ assessment = [0,1]
+ module = self.get_module_from_location(self.problem_location)
+ #Simulate a student saving an answer
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ status = module.handle_ajax("get_status", {})
+ #Mock a student submitting an assessment
+ assessment_dict = MockQueryDict()
+ assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ module.handle_ajax("save_assessment", assessment_dict)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
+ module.handle_ajax("get_status", {})
+
+ #Move to the next step in the problem
+ module.handle_ajax("next_problem", {})
+ module.get_html()
+ module.handle_ajax("get_combined_rubric", {})
+
+ module.handle_ajax("reset", {})
+
+ def test_open_ended_flow_correct(self):
+ assessment = [1,1]
+ module = self.get_module_from_location(self.problem_location)
+
+ #Simulate a student saving an answer
+ module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ status = module.handle_ajax("get_status", {})
+
+ #Mock a student submitting an assessment
+ assessment_dict = MockQueryDict()
+ assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ module.handle_ajax("save_assessment", assessment_dict)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
+ module.handle_ajax("get_status", {})
+
+ #Move to the next step in the problem
+ module.handle_ajax("next_problem", {})
+ module.get_html()
+ module.handle_ajax("get_combined_rubric", {})
diff --git a/common/test/data/open_ended/README.md b/common/test/data/open_ended/README.md
index 7fe58ac17f..ed1d5c771d 100644
--- a/common/test/data/open_ended/README.md
+++ b/common/test/data/open_ended/README.md
@@ -1 +1 @@
-This is a very very simple course, useful for debugging self assessment code.
+This is a very very simple course, useful for debugging open ended grading code.
diff --git a/common/test/data/open_ended/combinedopenended/SampleQuestion.xml b/common/test/data/open_ended/combinedopenended/SampleQuestion.xml
new file mode 100644
index 0000000000..5dbe285526
--- /dev/null
+++ b/common/test/data/open_ended/combinedopenended/SampleQuestion.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ Writing Applications
+
+
+
+
+ Language Conventions
+
+
+
+
+
+
+
Censorship in the Libraries
+
"All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us." --Katherine Paterson, Author
+
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
+
+
+
+
+
+
+
+ Enter essay here.
+ This is the answer.
+ {"grader_settings" : "ml_grading.conf", "problem_id" : "6.002x/Welcome/OETest"}
+
+
+
+
\ No newline at end of file
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
index ea7d5c420d..bf3ed687fb 100644
--- a/common/test/data/open_ended/course.xml
+++ b/common/test/data/open_ended/course.xml
@@ -1 +1 @@
-
+
From 3801f574a4f230fbd6ccb3e4a6d8aef2ffb23b79 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 16:00:07 -0400
Subject: [PATCH 019/123] Add in xqueue submission tests
---
.../open_ended_module.py | 2 +-
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../lib/xmodule/xmodule/tests/mock_server.py | 34 -------------------
.../xmodule/tests/test_combined_open_ended.py | 21 +++++++++---
4 files changed, 19 insertions(+), 40 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/tests/mock_server.py
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 8373700837..4a8604ac30 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -243,7 +243,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
})
# Submit request. When successful, 'msg' is the prior length of the queue
- (error, msg) = qinterface.send_to_queue(header=xheader,
+ qinterface.send_to_queue(header=xheader,
body=json.dumps(contents))
# State associated with the queueing request
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index b58308252e..0a2f22aa68 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -52,7 +52,7 @@ def test_system():
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
- xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
+ xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10, 'construct_callback' : Mock(side_effect="/")},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
xblock_model_data=lambda descriptor: descriptor._model_data,
anonymous_student_id='student',
diff --git a/common/lib/xmodule/xmodule/tests/mock_server.py b/common/lib/xmodule/xmodule/tests/mock_server.py
deleted file mode 100644
index d8ae2f9124..0000000000
--- a/common/lib/xmodule/xmodule/tests/mock_server.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from threading import Thread
-import socket
-import threading
-
-import SimpleHTTPServer
-import SocketServer
-
-class ThreadedRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
- def handle(self):
- data = self.request.recv(1024)
- cur_thread = threading.current_thread()
- response = "{}: {}".format(cur_thread.name, data)
- self.request.sendall(response)
- return
-
-class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
- pass
-
-def create_server(host,port):
- """
- Mock a server to be used for the open ended grading tests
- @param host: the hostname ie "localhost" or "127.0.0.1"
- @param port: the integer of the port to open a connection on
- @return: The created server object
- """
- server = ThreadedTCPServer((host,port), ThreadedRequestHandler)
-
- # Start a thread with the server -- that thread will then start one
- # more thread for each request
- server_thread = threading.Thread(target=server.serve_forever)
- # Exit the server thread when the main thread terminates
- server_thread.daemon = True
- server_thread.start()
- return server
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 0f7db73db4..a8127eec30 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -8,6 +8,7 @@ from mock import patch
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
+from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
@@ -520,9 +521,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
answer = "blah blah"
assessment = [0,1]
hint = "blah"
- test_server = create_server("127.0.0.1", 3034)
def setUp(self):
self.test_system = test_system()
+ self.test_system.xqueue['interface'] = Mock(
+ send_to_queue = Mock(side_effect=[1,"queued"])
+ )
@staticmethod
def get_import_system(load_error_modules=True):
@@ -592,6 +595,16 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
module.handle_ajax("get_status", {})
#Move to the next step in the problem
- module.handle_ajax("next_problem", {})
- module.get_html()
- module.handle_ajax("get_combined_rubric", {})
+ try:
+ module.handle_ajax("next_problem", {})
+ except GradingServiceError:
+ #This error is okay. We don't have a grading service to connect to!
+ pass
+ #Move to the next step in the problem
+ try:
+ module.get_html()
+ except GradingServiceError:
+ #This error is okay. We don't have a grading service to connect to!
+ pass
+
+ module.handle_ajax("get_combined_rubric", {})
From c39a21d833c51737413afee586444fa4a4ebde64 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 16:03:11 -0400
Subject: [PATCH 020/123] Remove old import
---
common/lib/xmodule/xmodule/tests/test_combined_open_ended.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index a8127eec30..d845a9a711 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -21,8 +21,6 @@ import capa.xqueue_interface as xqueue_interface
from datetime import datetime
import logging
-from mock_server import create_server
-
log = logging.getLogger(__name__)
from . import test_system
From 1e61bbd1c81d1cac3d26ea7df9f10ccfc10fd6bb Mon Sep 17 00:00:00 2001
From: Diana Huang
Date: Wed, 8 May 2013 16:06:08 -0400
Subject: [PATCH 021/123] Jobs page updates.
---
lms/templates/static_templates/jobs.html | 121 ++++++-----------------
1 file changed, 29 insertions(+), 92 deletions(-)
diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html
index 18ef1119e1..e96c44a2d5 100644
--- a/lms/templates/static_templates/jobs.html
+++ b/lms/templates/static_templates/jobs.html
@@ -374,35 +374,6 @@
-
-
-
-
DEVOPS ENGINEER – SYSTEMS ADMINISTRATOR
-
The Devop Engineers at edX help develop and maintain the infrastructure in AWS for all services and systems required to run edX. We're seeking a capable systems administrator who is unafraid of scripting languages and development to build out tools in order to improve the functionality of edX. The devops team primarily focuses on the provisioning, configuration, and deployment of services at edX. If you have a passion for automation and constant improvement then we want to hear from you. Our production environment is primarily built on Ubuntu (in AWS) and we use Puppet and Fabric to manage most of the environment.
-
In addition to the primary task of building infrastructure the Devops team supports the developers in a variety of other contexts, including helping with desktop development environments if required. We participate in on-call and emergency support and there will be occasional out of normal hours work required.
-
Responsibilities:
-
-
Work with developers and staff to maintain and improve the infrastructure of edX.
-
Assist where needed with other technical support tasks to support the fast moving pace of edX.
-
Rapidly diagnose and resolve faults with organization-wide servers and services, and communicate to users as appropriate.
-
-
Requirements:
-
-
Bachelor's degree in engineering or computer science. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
-
Three or more years of systems administration.
-
Must have an excellent working knowledge of Linux both as an end-user and as an administrator.
-
Must be adept in programming/scripting languages such as Python, Ruby, Bash.
-
Must be familiar with a configuration management system such as Puppet, Chef, Ansible.
-
Must have experience running web applications in a production environment.
-
Must have excellent personal interaction skills as the position requires interfacing with a wide range of people up to board level.
-
Ideally possesses experience with some of the following technologies: nginx, mysql, mongodb, django environments, splunk, git.
-
-
-
If you are interested in this position, please send an email to jobs@edx.org.
-
-
-
-
LEARNING SCIENCES ENGINEER
@@ -484,67 +455,34 @@ development and program management teams.
-
-
-
WEB DESIGNER, PRODUCT TEAM
-
-
EdX is looking for a Web Designer to join our Product Team and shape the experience of edX's online learning tools. With thousands and thousands of students and hundreds of professors using our software every day, our online learning tools have to sing. Our ideal candidates are passionate and picky about what makes a good user experience; sweat the mechanical, visual, and transactional details when designing; know how to bring an idea or project from a sketch on paper to being alive in a browser; can instinctually bring organization to a design meeting, deliverable, or project; and thrive on collaboration with colleagues and constant iteration/refinement.
-
-
As an edX Designer, you:
-
-
Have an innate sense of – and strong opinion about – good usability when it comes to web applications, and an ability to clearly articulate both.
-
Understand established interactive technologies and possess an undying thirst to learn about new ones.
-
Define and work within visual themes based on your excellent understanding of grids, typography, color, and design principles.
-
Marry design aesthetics to user experiences while keeping in mind accessibility, usability, and web standards.
-
Can use HTML5, CSS3, and DOM-manipulating JavaScript to represent your designs in the browser.
-
Conceptualize and articulate complex ideas to drive decisions, facilitate understanding, and reach consensus.
-
Document your thinking using appropriately chosen, informed deliverables such as sketches, wireframes, prototypes, site maps/flows, personas, style tiles, and design comps.
-
Have a perfectionist mindset, but won’t lose momentum in projects because of it.
-
Expertly present user experience and design recommendations to team members.
-
-
-
Requirements:
-
-
Have at least 2 years of professional, post-collegiate experience.
-
Have a BA, BS, BFA, or equivalent work experience in areas such as human-computer interaction, information science, graphic or industrial design, computer science, fine arts, social sciences such as psychology, or another related field. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
-
-
About the Product Design Team:
-
We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life. We enjoy holding Design Studio exercises, finding the right design tool to do the job efficiently, and our CSS preprocessors.
-
-
If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Designer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.
-
-
-
-
-
-
FRONT END DEVELOPER
-
edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.
-
As an edX Front End Developer, you:
-
-
Translate flat design comps, wireframes, and prototypes to production-ready interactive interfaces with joy and passion.
-
Are very familiar with cutting-edge front-end development practices and technology (CSS3, media queries, responsive web design, HTML5, etc.).
-
Write JavaScript without the use of a library while still being familiar with popular libraries such as jQuery.
-
Can abstract layouts, design patterns, and UI components while building out the interface to a product or application.
-
Appreciate that web standards, accessibility, and usability are essential to uphold.
-
Generally have experience with server-side templating and data extraction code while enjoying learning more from the development team.
-
Maintain the sanctity of a project's information architecture, interaction design, and visual design details while contributing to the effort.
-
Know how to test and refactor your code across browsers and with QA teams.
-
Work well with designers, developers, and colleagues.
-
Take pride in your communicative and collaborative abilities.
-
-
-
Front End Developers must also:
-
-
Have at least two years of professional, post-collegiate experience.
-
Have a BS, BFA or equivalent work experience. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
-
-
-
About the Product Design and Development Teams:
-
We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life.
-
-
If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.
-
-
+
+
+
TEST ENGINEER
+
EdX is looking for a Software Engineer in Test to help architect and implement improvements to our testing infrastructure and write code to validate and verify development and deployment of our MOOC platform.
+
You are an experienced professional who is passionate about and current with cutting edge methodologies and practices for delivering high quality software. For example, you understand and can articulate the difference between BDD and TDD. You champion for developers to be confident in the quality of their code by giving them the tools they need to create and execute their own tests. You write unit tests that follow best practices for each layer of an MVC architecture. You work side by side with the DevOps team to define environments and automate their buildouts.
+
Responsibilities:
+
+
Review software designs with a focus on code quality, risk, and testability
+
Build tools and frameworks that enable fellow engineers be more productive, write better code and test it themselves
+
Code test automation at all levels including class library, web application framework, javascript, and end-to-end
+
Enable metrics collection to measure adoption and expand the reach of the delivered tools
+
Fix framework bugs and improve test architecture, including adding required unit tests
+
Train and mentor other team members
+
+
Qualifications:
+
+
Excellent coding skills across a number of languages: Python or other high level programming languages, Javascript, bash, etc.
+
Experience in building test automation frameworks
+
Comfortable with source code in various languages (Python/Django, Ruby/Rails, Javascript/Backbone/JQuery, etc.)
+
Highly proficient in a Unix/Linux environment
+
Experience with database technologies from SQLite to MongoDB
+
Familiar with deployment automation (Puppet, Jenkins, AWS)
+
Open Source development experience preferred, extra points for sharing your GitHub / StackOverflow / etc. profile
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
+
+
@@ -559,11 +497,10 @@ development and program management teams.
E-mail your resume, cover letter and any other materials to jobs@edx.org
From f26a2585982c796e6aa1be0835882c070e1f155c Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:07:17 -0400
Subject: [PATCH 022/123] Add in test for grader reply
---
.../open_ended_module.py | 2 +-
.../xmodule/tests/test_combined_open_ended.py | 20 +++++++++++++++++--
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 4a8604ac30..afdfeef6de 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -516,7 +516,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
try:
feedback_dict = json.loads(score_result['feedback'])
except:
- pass
+ feedback_dict = score_result.get('feedback', '')
feedback_dicts = [feedback_dict]
grader_ids = [score_result['grader_id']]
submission_ids = [score_result['submission_id']]
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index d845a9a711..7cfec5fb62 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -598,11 +598,27 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
- #Move to the next step in the problem
try:
module.get_html()
except GradingServiceError:
#This error is okay. We don't have a grading service to connect to!
pass
- module.handle_ajax("get_combined_rubric", {})
+ module.handle_ajax("get_combined_rubric", {})
+
+ queue_reply = {
+ 'queuekey' : "",
+ 'xqueue_body' : json.dumps({
+ 'score' : 0,
+ 'feedback' : json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
+ 'grader_type' : "ML",
+ 'success' : True,
+ 'grader_id' : 1,
+ 'submission_id' : 1,
+ 'rubric_xml' : "Writing Applications0 Language Conventions 0",
+ 'rubric_scores_complete' : True,
+ })
+ }
+
+ module.handle_ajax("score_update", queue_reply)
+
From 4d759e9772b690c3ded7458bb7b9ed6eaecad6a1 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:15:57 -0400
Subject: [PATCH 023/123] Test full flow, including reset
---
.../combined_open_ended_modulev1.py | 12 ++++++++++++
.../xmodule/tests/test_combined_open_ended.py | 17 +++++++++++++++++
2 files changed, 29 insertions(+)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 12f90ed1b3..6767851d3a 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -803,6 +803,18 @@ class CombinedOpenEndedV1Module():
return progress_object
+ def out_of_sync_error(self, get, msg=''):
+ """
+ return dict out-of-sync error message, and also log.
+ """
+ #This is a dev_facing_error
+ log.warning("Combined module state out sync. state: %r, get: %r. %s",
+ self.state, get, msg)
+ #This is a student_facing_error
+ return {'success': False,
+ 'error': 'The problem state got out-of-sync. Please try reloading the page.'}
+
+
class CombinedOpenEndedV1Descriptor():
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 7cfec5fb62..3b8019290f 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -604,8 +604,10 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
#This error is okay. We don't have a grading service to connect to!
pass
+ #Try to get the rubric from the module
module.handle_ajax("get_combined_rubric", {})
+ #Make a fake reply from the queue
queue_reply = {
'queuekey' : "",
'xqueue_body' : json.dumps({
@@ -620,5 +622,20 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
})
}
+ module.handle_ajax("check_for_score", {})
+
+ #Update the module with the fake queue reply
module.handle_ajax("score_update", queue_reply)
+ #Get html and other data client will request
+ html = module.get_html()
+ legend = module.handle_ajax("get_legend", {})
+ status = module.handle_ajax("get_status", {})
+ legend = module.handle_ajax("skip_post_assessment", {})
+
+ #Get all results
+ legend = module.handle_ajax("get_results", {})
+
+ log.info(module.task_states)
+ #reset the problem
+ module.handle_ajax("reset", {})
From ff84545f3155c00e3824b79845e6ab3fac160b2e Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 18:49:28 -0400
Subject: [PATCH 024/123] Start to add peer grading tests, make dummy system a
separate thing
---
.../peer_grading_service.py | 27 +++++----
.../lib/xmodule/xmodule/tests/dummy_system.py | 55 ++++++++++++++++++
.../xmodule/tests/test_combined_open_ended.py | 57 ++-----------------
.../xmodule/tests/test_peer_grading.py | 49 ++++++++++++++++
.../test/data/open_ended/course/2012_Fall.xml | 1 +
.../peergrading/PeerGradingSample.xml | 1 +
.../data/open_ended/policies/2012_Fall.json | 5 +-
7 files changed, 131 insertions(+), 64 deletions(-)
create mode 100644 common/lib/xmodule/xmodule/tests/dummy_system.py
create mode 100644 common/lib/xmodule/xmodule/tests/test_peer_grading.py
create mode 100644 common/test/data/open_ended/peergrading/PeerGradingSample.xml
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 85c7a98132..19cc013cb7 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -100,29 +100,29 @@ without making actual service calls to the grading controller
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'submission_id': 1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
- 'max_score': 4})
+ 'max_score': 4}
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged):
- return json.dumps({'success': True})
+ return {'success': True}
def is_student_calibrated(self, problem_location, grader_id):
- return json.dumps({'success': True, 'calibrated': True})
+ return {'success': True, 'calibrated': True}
def show_calibration_essay(self, problem_location, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'submission_id': 1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
- 'max_score': 4})
+ 'max_score': 4}
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score,
@@ -130,10 +130,13 @@ class MockPeerGradingService(object):
return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id):
- return json.dumps({'success': True,
+ return {'success': True,
'problem_list': [
- json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
- 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
- json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
- 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
- ]})
+ {'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
+ 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5},
+ {'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
+ 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}
+ ]}
+
+ def get_data_for_location(self, problem_location, student_id):
+ return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
new file mode 100644
index 0000000000..b4ca6136eb
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -0,0 +1,55 @@
+from . import test_system
+import unittest
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+from xmodule.tests.test_export import DATA_DIR
+from fs.memoryfs import MemoryFS
+from mock import patch, Mock
+
+class DummySystem(ImportSystem):
+
+ @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
+ def __init__(self, load_error_modules, org, course):
+
+ xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
+ course_id = "/".join([org, course, 'test_run'])
+ course_dir = "test_dir"
+ policy = {}
+ error_tracker = Mock()
+ parent_tracker = Mock()
+
+ super(DummySystem, self).__init__(
+ xmlstore,
+ course_id,
+ course_dir,
+ policy,
+ error_tracker,
+ parent_tracker,
+ load_error_modules=load_error_modules,
+ )
+
+ def render_template(self, template, context):
+ raise Exception("Shouldn't be called")
+
+class DummySystemUser(object):
+ test_system = test_system()
+ @staticmethod
+ def get_import_system(org, course, load_error_modules=True):
+ '''Get a dummy system'''
+ return DummySystem(load_error_modules, org, course)
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ self.assertEquals(len(courses), 1)
+ return courses[0]
+
+ def get_module_from_location(self, location, course):
+ course = self.get_course(course)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 3b8019290f..fdbd37c6ab 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -5,6 +5,8 @@ import unittest
from fs.memoryfs import MemoryFS
from mock import patch
+from dummy_system import DummySystemUser
+
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
@@ -42,31 +44,6 @@ class MockQueryDict(dict):
return []
return default
-class DummySystem(ImportSystem):
-
- @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
- def __init__(self, load_error_modules):
-
- xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
- course_id = "/".join([ORG, COURSE, 'test_run'])
- course_dir = "test_dir"
- policy = {}
- error_tracker = Mock()
- parent_tracker = Mock()
-
- super(DummySystem, self).__init__(
- xmlstore,
- course_id,
- course_dir,
- policy,
- error_tracker,
- parent_tracker,
- load_error_modules=load_error_modules,
- )
-
- def render_template(self, template, context):
- raise Exception("Shouldn't be called")
-
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -514,7 +491,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
-class OpenEndedModuleXmlTest(unittest.TestCase):
+class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0,1]
@@ -525,37 +502,15 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
send_to_queue = Mock(side_effect=[1,"queued"])
)
- @staticmethod
- def get_import_system(load_error_modules=True):
- '''Get a dummy system'''
- return DummySystem(load_error_modules)
-
- def get_course(self, name):
- """Get a test course by directory name. If there's more than one, error."""
- print "Importing {0}".format(name)
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
- self.assertEquals(len(courses), 1)
- return courses[0]
-
- def get_module_from_location(self, location):
- course = self.get_course('open_ended')
- if not isinstance(location, Location):
- location = Location(location)
- descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
-
def test_open_ended_load_and_save(self):
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
module.handle_ajax("save_answer", {"student_answer" : self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
assessment = [0,1]
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer" : self.answer})
@@ -578,7 +533,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase):
def test_open_ended_flow_correct(self):
assessment = [1,1]
- module = self.get_module_from_location(self.problem_location)
+ module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer" : self.answer})
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
new file mode 100644
index 0000000000..01b3da0778
--- /dev/null
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -0,0 +1,49 @@
+import unittest
+from xmodule.modulestore import Location
+import json
+from lxml import etree
+from mock import Mock
+from . import test_system
+from dummy_system import DummySystem, DummySystemUser
+
+from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
+from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
+
+ORG = "edX"
+COURSE="open_ended"
+
+
+class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
+ location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "SampleQuestion"])
+ max_score = 1
+
+ definition = ""
+ descriptor = Mock(data=definition)
+
+ def setUp(self):
+ self.test_system = test_system()
+ self.test_system.open_ended_grading_interface = None
+ self.peer_grading = PeerGradingModule(self.test_system, self.location,self.descriptor, model_data={'data': self.definition})
+
+ def test_module_closed(self):
+ closed = self.peer_grading.closed()
+ self.assertEqual(closed, False)
+
+ def test_get_html(self):
+ html = self.peer_grading.get_html()
+
+ def test_get_data(self):
+ try:
+ success, data = self.peer_grading.query_data_for_location()
+ except GradingServiceError:
+ pass
+
+ def test_get_score(self):
+ score = self.peer_grading.get_score()
+
+ def test_get_max_score(self):
+ max_score = self.peer_grading.max_score()
+
+ def get_next_submission(self):
+ success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
\ No newline at end of file
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
index f2d16488a7..34369979ca 100644
--- a/common/test/data/open_ended/course/2012_Fall.xml
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -1,5 +1,6 @@
+
diff --git a/common/test/data/open_ended/peergrading/PeerGradingSample.xml b/common/test/data/open_ended/peergrading/PeerGradingSample.xml
new file mode 100644
index 0000000000..7e3afddf3a
--- /dev/null
+++ b/common/test/data/open_ended/peergrading/PeerGradingSample.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/test/data/open_ended/policies/2012_Fall.json b/common/test/data/open_ended/policies/2012_Fall.json
index 09b68ab400..8f8ba13437 100644
--- a/common/test/data/open_ended/policies/2012_Fall.json
+++ b/common/test/data/open_ended/policies/2012_Fall.json
@@ -9,6 +9,9 @@
"display_name": "Overview"
},
"combinedopenended/SampleQuestion": {
- "display_name": "Sample Question",
+ "display_name": "Sample Question"
},
+ "peergrading/PeerGradingSample": {
+ "display_name": "Sample Question"
+ }
}
From fd46ebd1fa950c86b8147093a644bd8b0d1561be Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:04:07 -0400
Subject: [PATCH 025/123] Move some functions, make notification tests more
robust
---
.../lib/xmodule/xmodule/tests/dummy_system.py | 15 +++++-
.../xmodule/tests/test_combined_open_ended.py | 25 ++--------
.../xmodule/tests/test_peer_grading.py | 10 ++--
common/test/data/open_ended/course.xml | 2 +-
lms/djangoapps/open_ended_grading/tests.py | 46 +++++++++++++++----
5 files changed, 57 insertions(+), 41 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
index b4ca6136eb..d0b7513321 100644
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -44,7 +44,6 @@ class DummySystemUser(object):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
courses = modulestore.get_courses()
self.modulestore = modulestore
- self.assertEquals(len(courses), 1)
return courses[0]
def get_module_from_location(self, location, course):
@@ -52,4 +51,16 @@ class DummySystemUser(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
+
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index fdbd37c6ab..665addefa2 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,21 +2,14 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from fs.memoryfs import MemoryFS
-from mock import patch
-
-from dummy_system import DummySystemUser
+from dummy_system import DummySystemUser, MockQueryDict
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
-from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
-from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
-
-from xmodule.tests.test_export import DATA_DIR
from lxml import etree
import capa.xqueue_interface as xqueue_interface
@@ -27,23 +20,11 @@ log = logging.getLogger(__name__)
from . import test_system
-ORG = 'test_org'
+ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
import test_util_open_ended
-class MockQueryDict(dict):
- """
- Mock a query set so that it can be used with default authorization
- """
- def getlist(self, key, default=None):
- try:
- return super(MockQueryDict, self).__getitem__(key)
- except KeyError:
- if default is None:
- return []
- return default
-
"""
Tests for the various pieces of the CombinedOpenEndedGrading system
@@ -492,7 +473,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['total'], 15.0)
class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
- problem_location = Location(["i4x", "edX", "oe_test", "combinedopenended", "SampleQuestion"])
+ problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0,1]
hint = "blah"
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 01b3da0778..3ab2e9301f 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -14,17 +14,13 @@ COURSE="open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
- location = Location(["i4x", "edX", "open_ended", "peergrading",
- "SampleQuestion"])
- max_score = 1
-
- definition = ""
- descriptor = Mock(data=definition)
+ problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "PeerGradingSample"])
def setUp(self):
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
- self.peer_grading = PeerGradingModule(self.test_system, self.location,self.descriptor, model_data={'data': self.definition})
+ self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
closed = self.peer_grading.closed()
diff --git a/common/test/data/open_ended/course.xml b/common/test/data/open_ended/course.xml
index bf3ed687fb..9848343f58 100644
--- a/common/test/data/open_ended/course.xml
+++ b/common/test/data/open_ended/course.xml
@@ -1 +1 @@
-
+
diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py
index 93d27d8e24..542c366cab 100644
--- a/lms/djangoapps/open_ended_grading/tests.py
+++ b/lms/djangoapps/open_ended_grading/tests.py
@@ -84,7 +84,13 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
+
self.assertTrue(d['success'])
self.assertEquals(d['submission_id'], self.mock_service.cnt)
self.assertIsNotNone(d['submission'])
@@ -112,7 +118,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data.update({'skipped' : True})
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'], str(d))
self.assertEquals(d['submission_id'], self.mock_service.cnt)
@@ -129,7 +139,11 @@ class TestStaffGradingService(LoginEnrollmentTestCase):
data = {}
r = self.check_for_post_code(200, url, data)
- d = json.loads(r.content)
+ d = r.content
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'], str(d))
self.assertIsNotNone(d['problem_list'])
@@ -179,7 +193,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.peer_module.get_next_submission(data)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
@@ -213,7 +231,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
qdict.keys = data.keys
r = self.peer_module.save_grade(qdict)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
def test_save_grade_missing_keys(self):
@@ -225,7 +247,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
def test_is_calibrated_success(self):
data = {'location': self.location}
r = self.peer_module.is_student_calibrated(data)
- d = json.loads(r)
+ d = r
+ try:
+ d = json.loads(d)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertTrue('calibrated' in d)
@@ -239,9 +265,11 @@ class TestPeerGradingService(LoginEnrollmentTestCase):
data = {'location': self.location}
r = self.peer_module.show_calibration_essay(data)
- d = json.loads(r)
- log.debug(d)
- log.debug(type(d))
+ d = r
+ try:
+ d = json.loads(r)
+ except Exception:
+ pass
self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt'])
From a2e5bd071b57f19fc36d0a9f8f41c0a46a3a27bf Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:17:14 -0400
Subject: [PATCH 026/123] Add in tests for peer grading module
---
.../peer_grading_service.py | 4 --
.../xmodule/tests/test_peer_grading.py | 38 ++++++++++++++++++-
2 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 19cc013cb7..418784f618 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -132,10 +132,6 @@ class MockPeerGradingService(object):
def get_problem_list(self, course_id, grader_id):
return {'success': True,
'problem_list': [
- {'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
- 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5},
- {'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
- 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5}
]}
def get_data_for_location(self, problem_location, student_id):
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 3ab2e9301f..3ecfc759e5 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -4,7 +4,7 @@ import json
from lxml import etree
from mock import Mock
from . import test_system
-from dummy_system import DummySystem, DummySystemUser
+from dummy_system import DummySystem, DummySystemUser, MockQueryDict
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
@@ -16,6 +16,17 @@ COURSE="open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingSample"])
+ calibrated_dict = {'location' : "blah"}
+ save_dict = MockQueryDict()
+ save_dict.update({
+ 'location' : "blah",
+ 'submission_id' : 1,
+ 'submission_key' : "",
+ 'score': 1,
+ 'feedback' : "",
+ 'rubric_scores[]' : [0,1],
+ 'submission_flagged': False,
+ })
def setUp(self):
self.test_system = test_system()
@@ -42,4 +53,27 @@ class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
max_score = self.peer_grading.max_score()
def get_next_submission(self):
- success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
\ No newline at end of file
+ success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
+
+ def test_save_grade(self):
+ self.peer_grading.save_grade(self.save_dict)
+
+ def test_is_student_calibrated(self):
+ calibrated_dict = {'location' : "blah"}
+ self.peer_grading.is_student_calibrated(self.calibrated_dict)
+
+ def test_show_calibration_essay(self):
+
+ self.peer_grading.show_calibration_essay(self.calibrated_dict)
+
+ def test_save_calibration_essay(self):
+ self.peer_grading.save_calibration_essay(self.save_dict)
+
+ def test_peer_grading_closed(self):
+ self.peer_grading.peer_grading_closed()
+
+ def test_peer_grading_problem(self):
+ self.peer_grading.peer_grading_problem(self.calibrated_dict)
+
+ def test_get_instance_state(self):
+ self.peer_grading.get_instance_state()
\ No newline at end of file
From 7b8b168f2e8f005f7e89e4e68012e5e7c0e85aa1 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Wed, 8 May 2013 19:21:12 -0400
Subject: [PATCH 027/123] Move the mockquerydict
---
common/lib/xmodule/xmodule/tests/dummy_system.py | 14 +-------------
.../xmodule/tests/test_combined_open_ended.py | 3 ++-
.../xmodule/xmodule/tests/test_peer_grading.py | 3 ++-
.../xmodule/tests/test_util_open_ended.py | 16 ++++++++++++++--
4 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
index d0b7513321..02fa0450f6 100644
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ b/common/lib/xmodule/xmodule/tests/dummy_system.py
@@ -51,16 +51,4 @@ class DummySystemUser(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
-
-class MockQueryDict(dict):
- """
- Mock a query set so that it can be used with default authorization
- """
- def getlist(self, key, default=None):
- try:
- return super(MockQueryDict, self).__getitem__(key)
- except KeyError:
- if default is None:
- return []
- return default
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 665addefa2..157d403ffe 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,7 +2,8 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from dummy_system import DummySystemUser, MockQueryDict
+from dummy_system import DummySystemUser
+from test_util_open_ended import MockQueryDict
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 3ecfc759e5..630f693333 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -4,7 +4,8 @@ import json
from lxml import etree
from mock import Mock
from . import test_system
-from dummy_system import DummySystem, DummySystemUser, MockQueryDict
+from dummy_system import DummySystem, DummySystemUser
+from test_util_open_ended import MockQueryDict
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index db580f1e0e..088a5af87d 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -1,5 +1,5 @@
OPEN_ENDED_GRADING_INTERFACE = {
- 'url': 'http://127.0.0.1:3033/',
+ 'url': 'blah/',
'username': 'incorrect',
'password': 'incorrect',
'staff_grading': 'staff_grading',
@@ -11,4 +11,16 @@ S3_INTERFACE = {
'aws_access_key': "",
'aws_secret_key': "",
"aws_bucket_name": "",
-}
\ No newline at end of file
+}
+
+class MockQueryDict(dict):
+ """
+ Mock a query set so that it can be used with default authorization
+ """
+ def getlist(self, key, default=None):
+ try:
+ return super(MockQueryDict, self).__getitem__(key)
+ except KeyError:
+ if default is None:
+ return []
+ return default
\ No newline at end of file
From ec5f239d91d0f89d4671902fda65a4baac5f00f7 Mon Sep 17 00:00:00 2001
From: Diana Huang
Date: Thu, 9 May 2013 09:12:07 -0400
Subject: [PATCH 028/123] Add in new Coordinator position.
---
lms/templates/static_templates/jobs.html | 37 ++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html
index e96c44a2d5..2ec8439e65 100644
--- a/lms/templates/static_templates/jobs.html
+++ b/lms/templates/static_templates/jobs.html
@@ -483,6 +483,42 @@ development and program management teams.
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
COORDINATOR OF UNIVERSITY AND BUSINESS AFFAIRS
+
EdX is looking for a Coordinator of External Affairs, to streamline, organize and maintain our efforts in Business Development and University Relations.
+
There are 4 primary areas of responsibility:
+
+
To ensure all visits to and from the edX offices by any partners and affiliates are managed, coordinated, and documented. This involves developing itineraries, booking flights and schedules, and managing meetings and events in concert with members of our executive team in University Relations and Business Development and our consortium of partners.
+
To maintain a database of partners and prospects and manage any data flows/reporting required.
+
To manage the information flow, recording activity on the edX Wiki page by synthesizing data and analysis from all visits and meetings and create updates on the edX Wiki page.
+
To act as a central point of contact for all relationship and event activity within this scope.
+
+
Detailed Responsibilities:
+
+
Provide support and coordinate activities for these 3 executives
+
Acquire strong user knowledge of related systems, processes and tools
+
Participate in the new partner on-boarding process
+
Provide an escalation point for Sales personnel for systems, procedures and policies
+
Maintain Salesforce database for client/partner set up and support information, generating reports as needed
+
Document proofreading, editing as directed for proposals, contracts, contact and call reports
+
Coordinate and manage travel, events and meetings, including invitations, RSVP’s, hotel/meeting space contracts, and providing event materials to attendees
+
+
Qualifications:
+
+
5-7 years of experience in a similar project/coordinator type position with progressively responsible administrative experience
+
Self-starter, possessing tenacity and a desire for challenges, not afraid to take risks, and the initiative to get things done with little direction
+
Superior interpersonal and communications skills, including concise writing and editing skills
+
Strong organizational skills to manage multiple competing priorities and projects with attention to detail
+
Exceptional ability to effectively interact with multiple external and internal stakeholders
+
Adept at analyzing complex issues with the ability to synthesize data and perform gap analyses
+
Performs well with a variety of disciplines while remaining effective in a high-volume, fast-pace start-up environment with high workload
+
Must be proficient in: MS PowerPoint, Word and Excel, Salesforce.com, and online tools such as Google docs and Wiki, and knowledge of Kanban is also helpful
+
+
+
If you are interested in this position, please send an email to jobs@edx.org.
E-mail your resume, cover letter and any other materials to jobs@edx.org
From 8323cc7c60174105361262b912ac1c25fa1fc93d Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:44:16 -0400
Subject: [PATCH 029/123] Refactor tests
---
.../lib/xmodule/xmodule/tests/dummy_system.py | 54 ----------
.../xmodule/tests/test_combined_open_ended.py | 102 +++++++++---------
.../xmodule/tests/test_peer_grading.py | 37 ++++---
.../xmodule/tests/test_util_open_ended.py | 27 ++++-
.../test/data/open_ended/course/2012_Fall.xml | 1 +
.../peergrading/PeerGradingScored.xml | 1 +
6 files changed, 98 insertions(+), 124 deletions(-)
delete mode 100644 common/lib/xmodule/xmodule/tests/dummy_system.py
create mode 100644 common/test/data/open_ended/peergrading/PeerGradingScored.xml
diff --git a/common/lib/xmodule/xmodule/tests/dummy_system.py b/common/lib/xmodule/xmodule/tests/dummy_system.py
deleted file mode 100644
index 02fa0450f6..0000000000
--- a/common/lib/xmodule/xmodule/tests/dummy_system.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from . import test_system
-import unittest
-from xmodule.modulestore import Location
-from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
-from xmodule.tests.test_export import DATA_DIR
-from fs.memoryfs import MemoryFS
-from mock import patch, Mock
-
-class DummySystem(ImportSystem):
-
- @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
- def __init__(self, load_error_modules, org, course):
-
- xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
- course_id = "/".join([org, course, 'test_run'])
- course_dir = "test_dir"
- policy = {}
- error_tracker = Mock()
- parent_tracker = Mock()
-
- super(DummySystem, self).__init__(
- xmlstore,
- course_id,
- course_dir,
- policy,
- error_tracker,
- parent_tracker,
- load_error_modules=load_error_modules,
- )
-
- def render_template(self, template, context):
- raise Exception("Shouldn't be called")
-
-class DummySystemUser(object):
- test_system = test_system()
- @staticmethod
- def get_import_system(org, course, load_error_modules=True):
- '''Get a dummy system'''
- return DummySystem(load_error_modules, org, course)
-
- def get_course(self, name):
- """Get a test course by directory name. If there's more than one, error."""
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
- return courses[0]
-
- def get_module_from_location(self, location, course):
- course = self.get_course(course)
- if not isinstance(location, Location):
- location = Location(location)
- descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 157d403ffe..2faecce08d 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -2,8 +2,7 @@ import json
from mock import Mock, MagicMock, ANY
import unittest
-from dummy_system import DummySystemUser
-from test_util_open_ended import MockQueryDict
+from test_util_open_ended import MockQueryDict, DummyModulestore
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
@@ -19,7 +18,7 @@ import logging
log = logging.getLogger(__name__)
-from . import test_system
+from .import test_system
ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
@@ -68,7 +67,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
@@ -115,7 +114,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(self.test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -142,12 +141,12 @@ class OpenEndedChildTest(unittest.TestCase):
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(self.static_data['max_score'])
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'correct')
+ 'correct')
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(0)
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'incorrect')
+ 'incorrect')
class OpenEndedModuleTest(unittest.TestCase):
@@ -202,7 +201,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
@@ -364,21 +363,21 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
- location,
- descriptor,
- model_data={'data': full_definition, 'weight': '1'})
+ location,
+ descriptor,
+ model_data={'data': full_definition, 'weight': '1'})
def setUp(self):
# TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- self.definition,
- self.descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("Tag")
@@ -433,12 +432,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
changed = combinedoe.update_task_states()
self.assertFalse(changed)
@@ -463,44 +462,46 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'task_xml': [self.task_xml1, self.task_xml2]}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=instance_state)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=instance_state)
score_dict = combinedoe.get_score()
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
-class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
+
+class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
- assessment = [0,1]
+ assessment = [0, 1]
hint = "blah"
+
def setUp(self):
self.test_system = test_system()
self.test_system.xqueue['interface'] = Mock(
- send_to_queue = Mock(side_effect=[1,"queued"])
- )
+ send_to_queue=Mock(side_effect=[1, "queued"])
+ )
def test_open_ended_load_and_save(self):
module = self.get_module_from_location(self.problem_location, COURSE)
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
- assessment = [0,1]
+ assessment = [0, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
status = module.handle_ajax("get_status", {})
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
- assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
@@ -514,16 +515,16 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
module.handle_ajax("reset", {})
def test_open_ended_flow_correct(self):
- assessment = [1,1]
+ assessment = [1, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
- module.handle_ajax("save_answer", {"student_answer" : self.answer})
+ module.handle_ajax("save_answer", {"student_answer": self.answer})
status = module.handle_ajax("get_status", {})
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
- assessment_dict.update({'assessment' : sum(assessment), 'score_list[]' : assessment})
+ assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
@@ -546,16 +547,17 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummySystemUser):
#Make a fake reply from the queue
queue_reply = {
- 'queuekey' : "",
- 'xqueue_body' : json.dumps({
- 'score' : 0,
- 'feedback' : json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.", "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
- 'grader_type' : "ML",
- 'success' : True,
- 'grader_id' : 1,
- 'submission_id' : 1,
- 'rubric_xml' : "Writing Applications0 Language Conventions 0",
- 'rubric_scores_complete' : True,
+ 'queuekey': "",
+ 'xqueue_body': json.dumps({
+ 'score': 0,
+ 'feedback': json.dumps({"spelling": "Spelling: Ok.", "grammar": "Grammar: Ok.",
+ "markup-text": " all of us can think of a book that we hope none of our children or any other children have taken off the shelf . but if i have the right to remove that book from the shelf that work i abhor then you also have exactly the same right and so does everyone else . and then we have no books left on the shelf for any of us . katherinepaterson , author write a persuasive essay to a newspaper reflecting your vies on censorship in libraries . do you believe that certain materials , such as books , music , movies , magazines , etc . , should be removed from the shelves if they are found offensive ? support your position with convincing arguments from your own experience , observations , and or reading . "}),
+ 'grader_type': "ML",
+ 'success': True,
+ 'grader_id': 1,
+ 'submission_id': 1,
+ 'rubric_xml': "Writing Applications0 Language Conventions 0",
+ 'rubric_scores_complete': True,
})
}
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 630f693333..d7cd7a4afd 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -1,33 +1,33 @@
import unittest
from xmodule.modulestore import Location
-import json
-from lxml import etree
-from mock import Mock
-from . import test_system
-from dummy_system import DummySystem, DummySystemUser
-from test_util_open_ended import MockQueryDict
+from .import test_system
+from test_util_open_ended import MockQueryDict, DummyModulestore
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
+import logging
+
+log = logging.getLogger(__name__)
+
ORG = "edX"
-COURSE="open_ended"
+COURSE = "open_ended"
-class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
+class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
- "PeerGradingSample"])
- calibrated_dict = {'location' : "blah"}
+ "PeerGradingSample"])
+ calibrated_dict = {'location': "blah"}
save_dict = MockQueryDict()
save_dict.update({
- 'location' : "blah",
- 'submission_id' : 1,
- 'submission_key' : "",
+ 'location': "blah",
+ 'submission_id': 1,
+ 'submission_key': "",
'score': 1,
- 'feedback' : "",
- 'rubric_scores[]' : [0,1],
+ 'feedback': "",
+ 'rubric_scores[]': [0, 1],
'submission_flagged': False,
- })
+ })
def setUp(self):
self.test_system = test_system()
@@ -54,17 +54,16 @@ class PeerGradingModuleTest(unittest.TestCase, DummySystemUser):
max_score = self.peer_grading.max_score()
def get_next_submission(self):
- success, next_submission = self.peer_grading.get_next_submission({'location' : 'blah'})
+ success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
def test_save_grade(self):
self.peer_grading.save_grade(self.save_dict)
def test_is_student_calibrated(self):
- calibrated_dict = {'location' : "blah"}
+ calibrated_dict = {'location': "blah"}
self.peer_grading.is_student_calibrated(self.calibrated_dict)
def test_show_calibration_essay(self):
-
self.peer_grading.show_calibration_essay(self.calibrated_dict)
def test_save_calibration_essay(self):
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 088a5af87d..38f083de38 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -1,3 +1,8 @@
+from .import test_system
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
+from xmodule.tests.test_export import DATA_DIR
+
OPEN_ENDED_GRADING_INTERFACE = {
'url': 'blah/',
'username': 'incorrect',
@@ -17,10 +22,30 @@ class MockQueryDict(dict):
"""
Mock a query set so that it can be used with default authorization
"""
+
def getlist(self, key, default=None):
try:
return super(MockQueryDict, self).__getitem__(key)
except KeyError:
if default is None:
return []
- return default
\ No newline at end of file
+ return default
+
+
+class DummyModulestore(object):
+ test_system = test_system()
+
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.modulestore = modulestore
+ return courses[0]
+
+ def get_module_from_location(self, location, course):
+ course = self.get_course(course)
+ if not isinstance(location, Location):
+ location = Location(location)
+ descriptor = self.modulestore.get_instance(course.id, location, depth=None)
+ return descriptor.xmodule(self.test_system)
\ No newline at end of file
diff --git a/common/test/data/open_ended/course/2012_Fall.xml b/common/test/data/open_ended/course/2012_Fall.xml
index 34369979ca..32c810174b 100644
--- a/common/test/data/open_ended/course/2012_Fall.xml
+++ b/common/test/data/open_ended/course/2012_Fall.xml
@@ -2,5 +2,6 @@
+
diff --git a/common/test/data/open_ended/peergrading/PeerGradingScored.xml b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
new file mode 100644
index 0000000000..b2380b1e1b
--- /dev/null
+++ b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
From 09d34a02ccd3d36c2b1b2d4b014d100a698c07e4 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:50:32 -0400
Subject: [PATCH 030/123] Add in some comments
---
.../xmodule/tests/test_combined_open_ended.py | 29 +++++++++
.../xmodule/tests/test_peer_grading.py | 59 ++++++++++++++++++-
.../xmodule/tests/test_util_open_ended.py | 5 +-
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 2faecce08d..1d67e94376 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -35,6 +35,9 @@ OpenEndedModule
class OpenEndedChildTest(unittest.TestCase):
+ """
+ Test the open ended child class
+ """
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
@@ -150,6 +153,9 @@ class OpenEndedChildTest(unittest.TestCase):
class OpenEndedModuleTest(unittest.TestCase):
+ """
+ Test the open ended module class
+ """
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
@@ -291,6 +297,9 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase):
+ """
+ Unit tests for the combined open ended xmodule
+ """
location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"])
definition_template = """
@@ -474,6 +483,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
+ """
+ Test the student flow in the combined open ended xmodule
+ """
problem_location = Location(["i4x", "edX", "open_ended", "combinedopenended", "SampleQuestion"])
answer = "blah blah"
assessment = [0, 1]
@@ -486,12 +498,23 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
)
def test_open_ended_load_and_save(self):
+ """
+ See if we can load the module and save an answer
+ @return:
+ """
+ #Load the module
module = self.get_module_from_location(self.problem_location, COURSE)
+
+ #Try saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer})
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
def test_open_ended_flow_reset(self):
+ """
+ Test the flow of the module if we complete the self assessment step and then reset
+ @return:
+ """
assessment = [0, 1]
module = self.get_module_from_location(self.problem_location, COURSE)
@@ -515,7 +538,13 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("reset", {})
def test_open_ended_flow_correct(self):
+ """
+ Test a two step problem where the student first goes through the self assessment step, and then the
+ open ended step.
+ @return:
+ """
assessment = [1, 1]
+ #Load the module
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index d7cd7a4afd..49c696f741 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -15,6 +15,10 @@ COURSE = "open_ended"
class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
+ """
+ Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
+ external grading service.
+ """
problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
"PeerGradingSample"])
calibrated_dict = {'location': "blah"}
@@ -30,50 +34,99 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
})
def setUp(self):
+ """
+ Create a peer grading module from a test system
+ @return:
+ """
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
+ """
+ Test if peer grading is closed
+ @return:
+ """
closed = self.peer_grading.closed()
self.assertEqual(closed, False)
def test_get_html(self):
+ """
+ Test to see if the module can be rendered
+ @return:
+ """
html = self.peer_grading.get_html()
def test_get_data(self):
+ """
+ Try getting data from the external grading service
+ @return:
+ """
try:
success, data = self.peer_grading.query_data_for_location()
except GradingServiceError:
pass
def test_get_score(self):
+ """
+ Test getting the score
+ @return:
+ """
score = self.peer_grading.get_score()
def test_get_max_score(self):
+ """
+ Test getting the max score
+ @return:
+ """
max_score = self.peer_grading.max_score()
def get_next_submission(self):
+ """
+ Test to see if we can get the next mock submission
+ @return:
+ """
success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
def test_save_grade(self):
+ """
+ Test if we can save the grade
+ @return:
+ """
self.peer_grading.save_grade(self.save_dict)
def test_is_student_calibrated(self):
+ """
+ Check to see if the student has calibrated yet
+ @return:
+ """
calibrated_dict = {'location': "blah"}
self.peer_grading.is_student_calibrated(self.calibrated_dict)
def test_show_calibration_essay(self):
+ """
+ Test showing the calibration essay
+ @return:
+ """
self.peer_grading.show_calibration_essay(self.calibrated_dict)
def test_save_calibration_essay(self):
+ """
+ Test saving the calibration essay
+ @return:
+ """
self.peer_grading.save_calibration_essay(self.save_dict)
- def test_peer_grading_closed(self):
- self.peer_grading.peer_grading_closed()
-
def test_peer_grading_problem(self):
+ """
+ See if we can render a single problem
+ @return:
+ """
self.peer_grading.peer_grading_problem(self.calibrated_dict)
def test_get_instance_state(self):
+ """
+ Get the instance state dict
+ @return:
+ """
self.peer_grading.get_instance_state()
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 38f083de38..f269a8a002 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -20,7 +20,7 @@ S3_INTERFACE = {
class MockQueryDict(dict):
"""
- Mock a query set so that it can be used with default authorization
+ Mock a query dict so that it can be used in test classes
"""
def getlist(self, key, default=None):
@@ -33,6 +33,9 @@ class MockQueryDict(dict):
class DummyModulestore(object):
+ """
+ A mixin that allows test classes to have convenience functions to get a module given a location
+ """
test_system = test_system()
def get_course(self, name):
From 8c12eb78c92c76b26b6d42ba16362e60a84f078a Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:54:57 -0400
Subject: [PATCH 031/123] Fix some exceptions
---
.../xmodule/open_ended_grading_classes/open_ended_module.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index afdfeef6de..a4b4afe499 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -496,8 +496,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
grader_types.append(score_result['grader_type'])
try:
feedback_dict = json.loads(score_result['feedback'][i])
- except:
- pass
+ except Exception:
+ feedback_dict = score_result['feedback'][i]
feedback_dicts.append(feedback_dict)
grader_ids.append(score_result['grader_id'][i])
submission_ids.append(score_result['submission_id'])
@@ -515,7 +515,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback_items = [feedback]
try:
feedback_dict = json.loads(score_result['feedback'])
- except:
+ except Exception:
feedback_dict = score_result.get('feedback', '')
feedback_dicts = [feedback_dict]
grader_ids = [score_result['grader_id']]
From 7a1ef62ee31fe7d52fc619514ac4dbc31f1bddce Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 09:56:02 -0400
Subject: [PATCH 032/123] Do some code reformatting
---
.../open_ended_module.py | 14 ++++-----
.../openendedchild.py | 12 ++++----
.../peer_grading_service.py | 30 +++++++++----------
3 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index a4b4afe499..266d332a7f 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -191,7 +191,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
}
(error, msg) = qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ body=json.dumps(contents))
#Convert error to a success value
success = True
@@ -225,8 +225,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str(len(self.child_history)))
xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
- lms_key=queuekey,
- queue_name=self.queue_name)
+ lms_key=queuekey,
+ queue_name=self.queue_name)
contents = self.payload.copy()
@@ -244,7 +244,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# Submit request. When successful, 'msg' is the prior length of the queue
qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ body=json.dumps(contents))
# State associated with the queueing request
queuestate = {'key': queuekey,
@@ -402,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not response_items['success']:
return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
- {'errors': feedback})
+ {'errors': feedback})
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
@@ -546,7 +546,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return ""
feedback_dict = self._parse_score_msg(self.child_history[-1].get('post_assessment', ""), system,
- join_feedback=join_feedback)
+ join_feedback=join_feedback)
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -711,7 +711,7 @@ class OpenEndedDescriptor():
template_dir_name = "openended"
def __init__(self, system):
- self.system =system
+ self.system = system
@classmethod
def definition_from_xml(cls, xml_object, system):
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index d5889636ed..2d8d3805f1 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -102,7 +102,7 @@ class OpenEndedChild(object):
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,
- system)
+ system)
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
@@ -180,8 +180,8 @@ class OpenEndedChild(object):
try:
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
- host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
- whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
+ host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
+ whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'$', '', re.sub(r'^
', '', clean_html))
except:
@@ -282,7 +282,7 @@ class OpenEndedChild(object):
"""
#This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
- self.child_state, get, msg)
+ self.child_state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
@@ -343,7 +343,7 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key,
- self.s3_interface)
+ self.s3_interface)
except:
log.exception("Could not upload image to S3.")
@@ -462,7 +462,7 @@ class OpenEndedChild(object):
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
- student_sub_count)
+ student_sub_count)
return success, allowed_to_submit, error_message
def get_eta(self):
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 418784f618..56bd1ec0a8 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -37,7 +37,7 @@ class PeerGradingService(GradingService):
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
- {'location': problem_location, 'grader_id': grader_id})
+ {'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores,
@@ -101,12 +101,12 @@ without making actual service calls to the grading controller
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
return {'success': True,
- 'submission_id': 1,
- 'submission_key': "",
- 'student_response': 'fake student response',
- 'prompt': 'fake submission prompt',
- 'rubric': 'fake rubric',
- 'max_score': 4}
+ 'submission_id': 1,
+ 'submission_key': "",
+ 'student_response': 'fake student response',
+ 'prompt': 'fake submission prompt',
+ 'rubric': 'fake rubric',
+ 'max_score': 4}
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key, rubric_scores, submission_flagged):
@@ -117,12 +117,12 @@ class MockPeerGradingService(object):
def show_calibration_essay(self, problem_location, grader_id):
return {'success': True,
- 'submission_id': 1,
- 'submission_key': '',
- 'student_response': 'fake student response',
- 'prompt': 'fake submission prompt',
- 'rubric': 'fake rubric',
- 'max_score': 4}
+ 'submission_id': 1,
+ 'submission_key': '',
+ 'student_response': 'fake student response',
+ 'prompt': 'fake submission prompt',
+ 'rubric': 'fake rubric',
+ 'max_score': 4}
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score,
@@ -131,8 +131,8 @@ class MockPeerGradingService(object):
def get_problem_list(self, course_id, grader_id):
return {'success': True,
- 'problem_list': [
- ]}
+ 'problem_list': [
+ ]}
def get_data_for_location(self, problem_location, student_id):
return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
From f9e97cb935e943baed19f1192e6dc371952245eb Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 10:05:48 -0400
Subject: [PATCH 033/123] Add test for proper saving
---
.../xmodule/tests/test_combined_open_ended.py | 5 ++++
.../xmodule/tests/test_peer_grading.py | 23 ++++++++++++++++++-
.../xmodule/tests/test_util_open_ended.py | 8 +++----
.../peergrading/PeerGradingScored.xml | 2 +-
4 files changed, 32 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 1d67e94376..48ea6e7911 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -496,6 +496,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"])
)
+ self.setup_modulestore(COURSE)
def test_open_ended_load_and_save(self):
"""
@@ -510,6 +511,10 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
task_one_json = json.loads(module.task_states[0])
self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+ module = self.get_module_from_location(self.problem_location, COURSE)
+ task_one_json = json.loads(module.task_states[0])
+ self.assertEqual(task_one_json['child_history'][0]['answer'], self.answer)
+
def test_open_ended_flow_reset(self):
"""
Test the flow of the module if we complete the self assessment step and then reset
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 49c696f741..036ab4a85b 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -40,6 +40,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
"""
self.test_system = test_system()
self.test_system.open_ended_grading_interface = None
+ self.setup_modulestore(COURSE)
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
def test_module_closed(self):
@@ -129,4 +130,24 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Get the instance state dict
@return:
"""
- self.peer_grading.get_instance_state()
\ No newline at end of file
+ self.peer_grading.get_instance_state()
+
+class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
+ """
+ Test peer grading xmodule at the unit level. More detailed tests are difficult, as the module relies on an
+ external grading service.
+ """
+ problem_location = Location(["i4x", "edX", "open_ended", "peergrading",
+ "PeerGradingScored"])
+ def setUp(self):
+ """
+ Create a peer grading module from a test system
+ @return:
+ """
+ self.test_system = test_system()
+ self.test_system.open_ended_grading_interface = None
+ self.setup_modulestore(COURSE)
+
+ def test_metadata_load(self):
+ peer_grading = self.get_module_from_location(self.problem_location, COURSE)
+ self.assertEqual(peer_grading.closed(), False)
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index f269a8a002..3737586232 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -38,12 +38,12 @@ class DummyModulestore(object):
"""
test_system = test_system()
+ def setup_modulestore(self, name):
+ self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.modulestore = modulestore
+ courses = self.modulestore.get_courses()
return courses[0]
def get_module_from_location(self, location, course):
diff --git a/common/test/data/open_ended/peergrading/PeerGradingScored.xml b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
index b2380b1e1b..6398a9b4c5 100644
--- a/common/test/data/open_ended/peergrading/PeerGradingScored.xml
+++ b/common/test/data/open_ended/peergrading/PeerGradingScored.xml
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
From f72659fa2e34fabda38260f75b250585e6e71800 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 10:15:07 -0400
Subject: [PATCH 034/123] Add in asserts
---
.../xmodule/tests/test_peer_grading.py | 25 ++++++++++++-------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index 036ab4a85b..a0877eab81 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -2,6 +2,7 @@ import unittest
from xmodule.modulestore import Location
from .import test_system
from test_util_open_ended import MockQueryDict, DummyModulestore
+import json
from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
@@ -63,10 +64,8 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Try getting data from the external grading service
@return:
"""
- try:
- success, data = self.peer_grading.query_data_for_location()
- except GradingServiceError:
- pass
+ success, data = self.peer_grading.query_data_for_location()
+ self.assertEqual(success, True)
def test_get_score(self):
"""
@@ -74,6 +73,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
score = self.peer_grading.get_score()
+ self.assertEquals(score['score'], None)
def test_get_max_score(self):
"""
@@ -81,6 +81,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
max_score = self.peer_grading.max_score()
+ self.assertEquals(max_score, None)
def get_next_submission(self):
"""
@@ -88,13 +89,15 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
success, next_submission = self.peer_grading.get_next_submission({'location': 'blah'})
+ self.assertEqual(success, True)
def test_save_grade(self):
"""
Test if we can save the grade
@return:
"""
- self.peer_grading.save_grade(self.save_dict)
+ response = self.peer_grading.save_grade(self.save_dict)
+ self.assertEqual(response['success'], True)
def test_is_student_calibrated(self):
"""
@@ -102,28 +105,32 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
@return:
"""
calibrated_dict = {'location': "blah"}
- self.peer_grading.is_student_calibrated(self.calibrated_dict)
+ response = self.peer_grading.is_student_calibrated(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_show_calibration_essay(self):
"""
Test showing the calibration essay
@return:
"""
- self.peer_grading.show_calibration_essay(self.calibrated_dict)
+ response = self.peer_grading.show_calibration_essay(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_save_calibration_essay(self):
"""
Test saving the calibration essay
@return:
"""
- self.peer_grading.save_calibration_essay(self.save_dict)
+ response = self.peer_grading.save_calibration_essay(self.save_dict)
+ self.assertEqual(response['success'], True)
def test_peer_grading_problem(self):
"""
See if we can render a single problem
@return:
"""
- self.peer_grading.peer_grading_problem(self.calibrated_dict)
+ response = self.peer_grading.peer_grading_problem(self.calibrated_dict)
+ self.assertEqual(response['success'], True)
def test_get_instance_state(self):
"""
From 682d06bcff1fd07b8ba158ee18562e0b729073b3 Mon Sep 17 00:00:00 2001
From: Vik Paruchuri
Date: Thu, 9 May 2013 11:27:44 -0400
Subject: [PATCH 035/123] Fix a lot of pep8 violations
---
.../xmodule/combined_open_ended_module.py | 13 ++--
.../combined_open_ended_modulev1.py | 12 ++-
.../grading_service_module.py | 3 +-
.../open_ended_module.py | 76 ++++++++++++-------
.../openendedchild.py | 15 ++--
.../xmodule/xmodule/peer_grading_module.py | 1 -
.../xmodule/tests/test_combined_open_ended.py | 56 +++++++-------
.../xmodule/tests/test_util_open_ended.py | 4 +-
8 files changed, 99 insertions(+), 81 deletions(-)
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index 67ff206e89..f4074283fe 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -104,11 +104,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem'
- js = {'coffee':
- [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
- resource_string(__name__, 'js/src/collapsible.coffee'),
- resource_string(__name__, 'js/src/javascript_loader.coffee'),
- ]}
+ js = {
+ 'coffee':
+ [
+ resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
+ resource_string(__name__, 'js/src/collapsible.coffee'),
+ resource_string(__name__, 'js/src/javascript_loader.coffee'),
+ ]
+ }
js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 6767851d3a..1404f52300 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -294,9 +294,8 @@ class CombinedOpenEndedV1Module():
if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number)
-
if (current_response_data['min_score_to_attempt'] > last_response_data['score']
- or current_response_data['max_score_to_attempt'] < last_response_data['score']):
+ or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE
self.ready_to_reset = True
@@ -662,9 +661,10 @@ class CombinedOpenEndedV1Module():
return {
'success': False,
#This is a student_facing_error
- 'error': ('You have attempted this question {0} times. '
- 'You are only allowed to attempt it {1} times.').format(
- self.student_attempts, self.attempts)
+ 'error': (
+ 'You have attempted this question {0} times. '
+ 'You are only allowed to attempt it {1} times.'
+ ).format(self.student_attempts, self.attempts)
}
self.state = self.INITIAL
self.ready_to_reset = False
@@ -815,7 +815,6 @@ class CombinedOpenEndedV1Module():
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
-
class CombinedOpenEndedV1Descriptor():
"""
Module for adding combined open ended questions
@@ -861,7 +860,6 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
-
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
index f3f6568b1e..b16f0618bb 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/grading_service_module.py
@@ -76,7 +76,6 @@ class GradingService(object):
return r.text
-
def _try_with_login(self, operation):
"""
Call operation(), which should return a requests response object. If
@@ -87,7 +86,7 @@ class GradingService(object):
"""
response = operation()
if (response.json
- and response.json.get('success') == False
+ and response.json.get('success') is False
and response.json.get('error') == 'login_required'):
# apparrently we aren't logged in. Try to fix that.
r = self._login()
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 266d332a7f..7ba046b2ad 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -72,7 +72,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self._parse(oeparam, self.child_prompt, self.child_rubric, system)
- if self.child_created == True and self.child_state == self.ASSESSING:
+ if self.child_created is True and self.child_state == self.ASSESSING:
self.child_created = False
self.send_to_grader(self.latest_answer(), system)
self.child_created = False
@@ -159,9 +159,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = int(survey_responses['score'])
except:
#This is a dev_facing_error
- error_message = ("Could not parse submission id, grader id, "
- "or feedback from message_post ajax call. Here is the message data: {0}".format(
- survey_responses))
+ error_message = (
+ "Could not parse submission id, grader id, "
+ "or feedback from message_post ajax call. "
+ "Here is the message data: {0}".format(survey_responses)
+ )
log.exception(error_message)
#This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
@@ -179,8 +181,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
queue_name=self.message_queue_name
)
- student_info = {'anonymous_student_id': anonymous_student_id,
- 'submission_time': qtime,
+ student_info = {
+ 'anonymous_student_id': anonymous_student_id,
+ 'submission_time': qtime,
}
contents = {
'feedback': feedback,
@@ -190,8 +193,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'student_info': json.dumps(student_info),
}
- (error, msg) = qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ (error, msg) = qinterface.send_to_queue(
+ header=xheader,
+ body=json.dumps(contents)
+ )
#Convert error to a success value
success = True
@@ -224,15 +229,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id +
str(len(self.child_history)))
- xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
+ xheader = xqueue_interface.make_xheader(
+ lms_callback_url=system.xqueue['construct_callback'](),
lms_key=queuekey,
- queue_name=self.queue_name)
+ queue_name=self.queue_name
+ )
contents = self.payload.copy()
# Metadata related to the student submission revealed to the external grader
- student_info = {'anonymous_student_id': anonymous_student_id,
- 'submission_time': qtime,
+ student_info = {
+ 'anonymous_student_id': anonymous_student_id,
+ 'submission_time': qtime,
}
#Update contents with student response and student info
@@ -243,12 +251,16 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
})
# Submit request. When successful, 'msg' is the prior length of the queue
- qinterface.send_to_queue(header=xheader,
- body=json.dumps(contents))
+ qinterface.send_to_queue(
+ header=xheader,
+ body=json.dumps(contents)
+ )
# State associated with the queueing request
- queuestate = {'key': queuekey,
- 'time': qtime, }
+ queuestate = {
+ 'key': queuekey,
+ 'time': qtime,
+ }
return True
def _update_score(self, score_msg, queuekey, system):
@@ -302,11 +314,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# We want to display available feedback in a particular order.
# This dictionary specifies which goes first--lower first.
- priorities = {# These go at the start of the feedback
- 'spelling': 0,
- 'grammar': 1,
- # needs to be after all the other feedback
- 'markup_text': 3}
+ priorities = {
+ # These go at the start of the feedback
+ 'spelling': 0,
+ 'grammar': 1,
+ # needs to be after all the other feedback
+ 'markup_text': 3
+ }
do_not_render = ['topicality', 'prompt-overlap']
default_priority = 2
@@ -393,7 +407,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback = ""
feedback = self._convert_longform_feedback_to_html(response_items)
rubric_scores = []
- if response_items['rubric_scores_complete'] == True:
+ if response_items['rubric_scores_complete'] is True:
rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_dict = rubric_renderer.render_rubric(response_items['rubric_xml'])
success = rubric_dict['success']
@@ -401,8 +415,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_scores = rubric_dict['rubric_scores']
if not response_items['success']:
- return system.render_template("{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
- {'errors': feedback})
+ return system.render_template(
+ "{0}/open_ended_error.html".format(self.TEMPLATE_DIR),
+ {'errors': feedback}
+ )
feedback_template = system.render_template("{0}/open_ended_feedback.html".format(self.TEMPLATE_DIR), {
'grader_type': response_items['grader_type'],
@@ -545,8 +561,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not self.child_history:
return ""
- feedback_dict = self._parse_score_msg(self.child_history[-1].get('post_assessment', ""), system,
- join_feedback=join_feedback)
+ feedback_dict = self._parse_score_msg(
+ self.child_history[-1].get('post_assessment', ""),
+ system,
+ join_feedback=join_feedback
+ )
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -734,8 +753,9 @@ class OpenEndedDescriptor():
"""Assumes that xml_object has child k"""
return xml_object.xpath(k)[0]
- return {'oeparam': parse('openendedparam')}
-
+ return {
+ 'oeparam': parse('openendedparam')
+ }
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index 2d8d3805f1..7dc8d99451 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -101,8 +101,9 @@ class OpenEndedChild(object):
# completion (doesn't matter if you self-assessed correct/incorrect).
if system.open_ended_grading_interface:
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
- self.controller_qs = controller_query_service.ControllerQueryService(system.open_ended_grading_interface,
- system)
+ self.controller_qs = controller_query_service.ControllerQueryService(
+ system.open_ended_grading_interface,system
+ )
else:
self.peer_gs = MockPeerGradingService()
self.controller_qs = None
@@ -180,8 +181,8 @@ class OpenEndedChild(object):
try:
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
- host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
- whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
+ host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
+ whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'
$', '', re.sub(r'^
', '', clean_html))
except:
@@ -282,7 +283,7 @@ class OpenEndedChild(object):
"""
#This is a dev_facing_error
log.warning("Open ended child state out sync. state: %r, get: %r. %s",
- self.child_state, get, msg)
+ self.child_state, get, msg)
#This is a student_facing_error
return {'success': False,
'error': 'The problem state got out-of-sync. Please try reloading the page.'}
@@ -343,7 +344,7 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key,
- self.s3_interface)
+ self.s3_interface)
except:
log.exception("Could not upload image to S3.")
@@ -462,7 +463,7 @@ class OpenEndedChild(object):
allowed_to_submit = False
#This is a student_facing_error
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
- student_sub_count)
+ student_sub_count)
return success, allowed_to_submit, error_message
def get_eta(self):
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
index 1ad31922f5..eebfbe22e5 100644
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ b/common/lib/xmodule/xmodule/peer_grading_module.py
@@ -498,7 +498,6 @@ class PeerGradingModule(PeerGradingFields, XModule):
log.error("Problem {0} does not exist in this course".format(location))
raise
-
for problem in problem_list:
problem_location = problem['location']
descriptor = _find_corresponding_module_for_location(problem_location)
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 48ea6e7911..d8f4fbbca1 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -18,7 +18,7 @@ import logging
log = logging.getLogger(__name__)
-from .import test_system
+from . import test_system
ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data
@@ -70,8 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
-
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self):
answer = self.openendedchild.latest_answer()
@@ -117,7 +116,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(self.test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -144,12 +143,12 @@ class OpenEndedChildTest(unittest.TestCase):
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(self.static_data['max_score'])
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'correct')
+ 'correct')
self.openendedchild.new_history_entry(new_answer)
self.openendedchild.record_latest_score(0)
self.assertEqual(self.openendedchild.is_last_response_correct(),
- 'incorrect')
+ 'incorrect')
class OpenEndedModuleTest(unittest.TestCase):
@@ -207,7 +206,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
- self.definition, self.descriptor, self.static_data, self.metadata)
+ self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
get = {'feedback': 'feedback text',
@@ -372,21 +371,20 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
- location,
- descriptor,
- model_data={'data': full_definition, 'weight': '1'})
-
+ location,
+ descriptor,
+ model_data={'data': full_definition, 'weight': '1'})
def setUp(self):
# TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- self.definition,
- self.descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("Tag")
@@ -441,12 +439,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric), 'task_xml': xml}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=self.static_data)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=self.static_data)
changed = combinedoe.update_task_states()
self.assertFalse(changed)
@@ -471,12 +469,12 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'task_xml': [self.task_xml1, self.task_xml2]}
descriptor = Mock(data=definition)
combinedoe = CombinedOpenEndedV1Module(self.test_system,
- self.location,
- definition,
- descriptor,
- static_data=self.static_data,
- metadata=self.metadata,
- instance_state=instance_state)
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=instance_state)
score_dict = combinedoe.get_score()
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index 3737586232..42d6410ebd 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -18,11 +18,11 @@ S3_INTERFACE = {
"aws_bucket_name": "",
}
+
class MockQueryDict(dict):
"""
Mock a query dict so that it can be used in test classes
"""
-
def getlist(self, key, default=None):
try:
return super(MockQueryDict, self).__getitem__(key)
@@ -51,4 +51,4 @@ class DummyModulestore(object):
if not isinstance(location, Location):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
- return descriptor.xmodule(self.test_system)
\ No newline at end of file
+ return descriptor.xmodule(self.test_system)
From e5b18a56885d2bd7e537fdabf164b2206886a78c Mon Sep 17 00:00:00 2001
From: Diana Huang
Date: Thu, 9 May 2013 13:58:25 -0400
Subject: [PATCH 036/123] Put Front End Developer back in.
---
lms/templates/static_templates/jobs.html | 31 ++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/lms/templates/static_templates/jobs.html b/lms/templates/static_templates/jobs.html
index 2ec8439e65..d041d10737 100644
--- a/lms/templates/static_templates/jobs.html
+++ b/lms/templates/static_templates/jobs.html
@@ -454,6 +454,37 @@ development and program management teams.
If you are interested in this position, please send an email to jobs@edx.org.
+
+
+
FRONT END DEVELOPER
+
edX is looking for a Front End Developer to join our Product and Engineering Teams to shape the experience of all of edX's online learning tools. Thousands of students learn with us every day – the way they connect with their courses, their professors and edX is through our ever more powerful front end. Our ideal candidates not only know modern front end development best practices, but make organization standards and teach others with them; sweat the mechanical, visual, and transactional details when bring a design to life in the browser; can instinctually bring organization to their HTML/CSS/JavaScript, documentation, or project; and thrive on collaborating with both designers and developers throughout a project's lifecycle.
+
As an edX Front End Developer, you:
+
+
Translate flat design comps, wireframes, and prototypes to production-ready interactive interfaces with joy and passion.
+
Are very familiar with cutting-edge front-end development practices and technology (CSS3, media queries, responsive web design, HTML5, etc.).
+
Write JavaScript without the use of a library while still being familiar with popular libraries such as jQuery.
+
Can abstract layouts, design patterns, and UI components while building out the interface to a product or application.
+
Appreciate that web standards, accessibility, and usability are essential to uphold.
+
Generally have experience with server-side templating and data extraction code while enjoying learning more from the development team.
+
Maintain the sanctity of a project's information architecture, interaction design, and visual design details while contributing to the effort.
+
Know how to test and refactor your code across browsers and with QA teams.
+
Work well with designers, developers, and colleagues.
+
Take pride in your communicative and collaborative abilities.
+
+
+
Front End Developers must also:
+
+
Have at least two years of professional, post-collegiate experience.
+
Have a BS, BFA or equivalent work experience. But we're all about education, so let us know how you gained what you need to succeed in this role: projects after completing 6.00x or CS50x, Xbox cheevos, on-line guilds led, large scale innovations championed.
+
+
+
About the Product Design and Development Teams:
+
We are a small team with a startup, lean culture, committed to building tools that help our users learn and teach online. Working alongside developers, course staff, product owners, and project stakeholders, our Designers shepherd the experience of an idea or tool through research and strategy phases and lead the Information Architecture, Interaction Design, Visual Design, and Front End Development efforts in bringing that experience to life.
+
+
If you wish to apply, please send your resume (PDF, text, or Word Doc), a thoughtful email that includes specifics about how your previous experience matches the Front End Developer role at edX, and online samples of your work to jobs@edx.org. Candidates who do not provide these will not be considered. EdX is open to considering candidates outside of the Boston/Cambridge, MA area who are willing to relocate.
+
+
+
From ea859bd6c78fbcf07df5138d61b2ea1408ada651 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Thu, 9 May 2013 15:26:01 -0400
Subject: [PATCH 037/123] Filter out the template course from the mongo
modulestore get_courses function
---
.../management/commands/update_templates.py | 3 ++-
.../contentstore/tests/test_contentstore.py | 2 +-
common/djangoapps/terrain/course_helpers.py | 2 +-
common/lib/xmodule/xmodule/modulestore/mongo.py | 10 +++++++++-
.../xmodule/modulestore/tests/django_utils.py | 2 +-
.../xmodule/xmodule/modulestore/tests/test_mongo.py | 10 ++++++++++
common/lib/xmodule/xmodule/templates.py | 13 ++++++-------
7 files changed, 30 insertions(+), 12 deletions(-)
diff --git a/cms/djangoapps/contentstore/management/commands/update_templates.py b/cms/djangoapps/contentstore/management/commands/update_templates.py
index e94fee64b8..36348314b9 100644
--- a/cms/djangoapps/contentstore/management/commands/update_templates.py
+++ b/cms/djangoapps/contentstore/management/commands/update_templates.py
@@ -1,4 +1,5 @@
from xmodule.templates import update_templates
+from xmodule.modulestore.django import modulestore
from django.core.management.base import BaseCommand
@@ -6,4 +7,4 @@ class Command(BaseCommand):
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
def handle(self, *args, **options):
- update_templates()
+ update_templates(modulestore('direct'))
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 07b7032e60..844ba87a11 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -937,7 +937,7 @@ class TemplateTestCase(ModuleStoreTestCase):
self.assertIsNotNone(verify_create)
# now run cleanup
- update_templates()
+ update_templates(modulestore('direct'))
# now try to find dangling template, it should not be in DB any longer
asserted = False
diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py
index f0df456c80..9d6837ae86 100644
--- a/common/djangoapps/terrain/course_helpers.py
+++ b/common/djangoapps/terrain/course_helpers.py
@@ -137,4 +137,4 @@ def clear_courses():
# $ mongo test_xmodule --eval "db.dropDatabase()"
_MODULESTORES = {}
modulestore().collection.drop()
- update_templates()
+ update_templates(modulestore('direct'))
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index c8256422f8..24df17b15b 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -476,7 +476,15 @@ class MongoModuleStore(ModuleStoreBase):
'''
# TODO (vshnayder): Why do I have to specify i4x here?
course_filter = Location("i4x", category="course")
- return self.get_items(course_filter)
+ return [
+ course
+ for course
+ in self.get_items(course_filter)
+ if not (
+ course.location.org == 'edx' and
+ course.location.course == 'templates'
+ )
+ ]
def _find_one(self, location):
'''Look for a given location in the collection. If revision is not
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
index 753cbfac4f..98523e9b15 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py
@@ -42,7 +42,7 @@ class ModuleStoreTestCase(TestCase):
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
- update_templates()
+ update_templates(modulestore)
@classmethod
def setUpClass(cls):
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
index 061d70d09f..6332ade04f 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -7,6 +7,7 @@ from pprint import pprint
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.templates import update_templates
from .test_modulestore import check_path_to_location
from . import DATA_DIR
@@ -45,6 +46,7 @@ class TestMongoModuleStore(object):
# Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses)
+ update_templates(store)
return store
@staticmethod
@@ -103,3 +105,11 @@ class TestMongoModuleStore(object):
def test_path_to_location(self):
'''Make sure that path_to_location works'''
check_path_to_location(self.store)
+
+ def test_get_courses_has_no_templates(self):
+ courses = self.store.get_courses()
+ for course in courses:
+ assert_false(
+ course.location.org == 'edx' and course.location.course == 'templates',
+ '{0} is a template course'.format(course)
+ )
diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py
index eaf821155e..f4e37ab0d5 100644
--- a/common/lib/xmodule/xmodule/templates.py
+++ b/common/lib/xmodule/xmodule/templates.py
@@ -19,7 +19,6 @@ from collections import defaultdict
from .x_module import XModuleDescriptor
from .mako_module import MakoDescriptorSystem
from .modulestore import Location
-from .modulestore.django import modulestore
log = logging.getLogger(__name__)
@@ -50,7 +49,7 @@ class TemplateTestSystem(MakoDescriptorSystem):
)
-def update_templates():
+def update_templates(modulestore):
"""
Updates the set of templates in the modulestore with all templates currently
available from the installed plugins
@@ -58,7 +57,7 @@ def update_templates():
# cdodge: build up a list of all existing templates. This will be used to determine which
# templates have been removed from disk - and thus we need to remove from the DB
- templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None])
+ templates_to_delete = modulestore.get_items(['i4x', 'edx', 'templates', None, None, None])
for category, templates in all_templates().items():
for template in templates:
@@ -86,9 +85,9 @@ def update_templates():
), exc_info=True)
continue
- modulestore('direct').update_item(template_location, template.data)
- modulestore('direct').update_children(template_location, template.children)
- modulestore('direct').update_metadata(template_location, template.metadata)
+ modulestore.update_item(template_location, template.data)
+ modulestore.update_children(template_location, template.children)
+ modulestore.update_metadata(template_location, template.metadata)
# remove template from list of templates to delete
templates_to_delete = [t for t in templates_to_delete if t.location != template_location]
@@ -97,4 +96,4 @@ def update_templates():
if len(templates_to_delete) > 0:
logging.debug('deleting dangling templates = {0}'.format(templates_to_delete))
for template in templates_to_delete:
- modulestore('direct').delete_item(template.location)
+ modulestore.delete_item(template.location)
From 0bea50ede17a190a8f0a9ba61e71535be639090b Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Thu, 9 May 2013 16:24:21 -0400
Subject: [PATCH 038/123] start refactoring views.py
---
cms/djangoapps/contentstore/views.py | 1685 -----------------
cms/djangoapps/contentstore/views/__init__.py | 17 +
cms/djangoapps/contentstore/views/access.py | 39 +
cms/djangoapps/contentstore/views/assets.py | 118 ++
.../contentstore/views/checklist.py | 98 +
cms/djangoapps/contentstore/views/course.py | 202 ++
cms/djangoapps/contentstore/views/error.py | 21 +
cms/djangoapps/contentstore/views/item.py | 123 ++
cms/djangoapps/contentstore/views/preview.py | 156 ++
cms/djangoapps/contentstore/views/public.py | 50 +
cms/djangoapps/contentstore/views/requests.py | 91 +
.../contentstore/views/session_kv_store.py | 27 +
cms/djangoapps/contentstore/views/user.py | 107 ++
13 files changed, 1049 insertions(+), 1685 deletions(-)
delete mode 100644 cms/djangoapps/contentstore/views.py
create mode 100644 cms/djangoapps/contentstore/views/__init__.py
create mode 100644 cms/djangoapps/contentstore/views/access.py
create mode 100644 cms/djangoapps/contentstore/views/assets.py
create mode 100644 cms/djangoapps/contentstore/views/checklist.py
create mode 100644 cms/djangoapps/contentstore/views/course.py
create mode 100644 cms/djangoapps/contentstore/views/error.py
create mode 100644 cms/djangoapps/contentstore/views/item.py
create mode 100644 cms/djangoapps/contentstore/views/preview.py
create mode 100644 cms/djangoapps/contentstore/views/public.py
create mode 100644 cms/djangoapps/contentstore/views/requests.py
create mode 100644 cms/djangoapps/contentstore/views/session_kv_store.py
create mode 100644 cms/djangoapps/contentstore/views/user.py
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
deleted file mode 100644
index 824d2119f1..0000000000
--- a/cms/djangoapps/contentstore/views.py
+++ /dev/null
@@ -1,1685 +0,0 @@
-from util.json_request import expect_json
-import json
-import logging
-import os
-import sys
-import time
-import tarfile
-import shutil
-from collections import defaultdict
-from uuid import uuid4
-from path import path
-from xmodule.modulestore.xml_exporter import export_to_xml
-from tempfile import mkdtemp
-from django.core.servers.basehttp import FileWrapper
-from django.core.files.temp import NamedTemporaryFile
-
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
-from django.http import HttpResponseNotFound
-from django.contrib.auth.decorators import login_required
-from django.core.exceptions import PermissionDenied
-from django.core.context_processors import csrf
-from django_future.csrf import ensure_csrf_cookie
-from django.core.urlresolvers import reverse
-from django.conf import settings
-
-from xmodule.modulestore import Location
-from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
-from xmodule.modulestore.inheritance import own_metadata
-from xblock.core import Scope
-from xblock.runtime import KeyValueStore, DbModel, InvalidScopeError
-from xmodule.x_module import ModuleSystem
-from xmodule.error_module import ErrorDescriptor
-from xmodule.errortracker import exc_info_to_str
-import static_replace
-from external_auth.views import ssl_login_shortcut
-from xmodule.modulestore.mongo import MongoUsage
-
-from mitxmako.shortcuts import render_to_response, render_to_string
-from xmodule.modulestore.django import modulestore
-from xmodule_modifiers import replace_static_urls, wrap_xmodule
-from xmodule.exceptions import NotFoundError, ProcessingError
-from functools import partial
-
-from xmodule.contentstore.django import contentstore
-from xmodule.contentstore.content import StaticContent
-from xmodule.util.date_utils import get_default_time_display
-
-from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
-from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
-from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
-from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
- UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
- remove_open_ended_panel_tab
-
-from xmodule.modulestore.xml_importer import import_from_xml
-from contentstore.course_info_model import get_course_updates, \
- update_course_updates, delete_course_update
-from cache_toolbox.core import del_cached_content
-from contentstore.module_info_model import get_module_info, set_module_info
-from models.settings.course_details import CourseDetails, \
- CourseSettingsEncoder
-from models.settings.course_grading import CourseGradingModel
-from contentstore.utils import get_modulestore
-from django.shortcuts import redirect
-from models.settings.course_metadata import CourseMetadata
-
-# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
-
-log = logging.getLogger(__name__)
-
-
-COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
-
-OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
-ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
-ADVANCED_COMPONENT_CATEGORY = 'advanced'
-ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
-
-# cdodge: these are categories which should not be parented, they are detached from the hierarchy
-DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
-
-# ==== Public views ==================================================
-
-@ensure_csrf_cookie
-def signup(request):
- """
- Display the signup form.
- """
- csrf_token = csrf(request)['csrf_token']
- return render_to_response('signup.html', {'csrf': csrf_token})
-
-
-def old_login_redirect(request):
- '''
- Redirect to the active login url.
- '''
- return redirect('login', permanent=True)
-
-
-@ssl_login_shortcut
-@ensure_csrf_cookie
-def login_page(request):
- """
- Display the login form.
- """
- csrf_token = csrf(request)['csrf_token']
- return render_to_response('login.html', {
- 'csrf': csrf_token,
- 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
- })
-
-
-def howitworks(request):
- if request.user.is_authenticated():
- return index(request)
- else:
- return render_to_response('howitworks.html', {})
-
-
-# static/proof-of-concept views
-def ux_alerts(request):
- return render_to_response('ux-alerts.html', {})
-
-
-# ==== Views for any logged-in user ==================================
-
-
-@login_required
-@ensure_csrf_cookie
-def index(request):
- """
- List all courses available to the logged in user
- """
- courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
-
- # filter out courses that we don't have access too
- def course_filter(course):
- return (has_access(request.user, course.location)
- and course.location.course != 'templates'
- and course.location.org != ''
- and course.location.course != ''
- and course.location.name != '')
- courses = filter(course_filter, courses)
-
- return render_to_response('index.html', {
- 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.display_name,
- get_url_reverse('CourseOutline', course),
- get_lms_link_for_item(course.location, course_id=course.location.course_id))
- for course in courses],
- 'user': request.user,
- 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
- })
-
-
-# ==== Views with per-item permissions================================
-
-
-def has_access(user, location, role=STAFF_ROLE_NAME):
- '''
- Return True if user allowed to access this piece of data
- Note that the CMS permissions model is with respect to courses
- There is a super-admin permissions if user.is_staff is set
- Also, since we're unifying the user database between LMS and CAS,
- I'm presuming that the course instructor (formally known as admin)
- will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
- has all the rights that STAFF do
- '''
- course_location = get_course_location_for_item(location)
- _has_access = is_user_in_course_group_role(user, course_location, role)
- # if we're not in STAFF, perhaps we're in INSTRUCTOR groups
- if not _has_access and role == STAFF_ROLE_NAME:
- _has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
- return _has_access
-
-
-@login_required
-@ensure_csrf_cookie
-def course_index(request, org, course, name):
- """
- Display an editable course overview.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- lms_link = get_lms_link_for_item(location)
-
- upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
-
- course = modulestore().get_item(location, depth=3)
- sections = course.get_children()
-
- return render_to_response('overview.html', {
- 'active_tab': 'courseware',
- 'context_course': course,
- 'lms_link': lms_link,
- 'sections': sections,
- 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
- 'parent_location': course.location,
- 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
- 'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
- 'upload_asset_callback_url': upload_asset_callback_url,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
- })
-
-
-@login_required
-def edit_subsection(request, location):
- # check that we have permissions to edit this item
- course = get_course_for_item(location)
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location, depth=1)
-
- lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
- preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
-
- # make sure that location references a 'sequential', otherwise return BadRequest
- if item.location.category != 'sequential':
- return HttpResponseBadRequest()
-
- parent_locs = modulestore().get_parent_locations(location, None)
-
- # we're for now assuming a single parent
- if len(parent_locs) != 1:
- logging.error('Multiple (or none) parents have been found for {0}'.format(location))
-
- # this should blow up if we don't find any parents, which would be erroneous
- parent = modulestore().get_item(parent_locs[0])
-
- # remove all metadata from the generic dictionary that is presented in a more normalized UI
-
- policy_metadata = dict(
- (field.name, field.read_from(item))
- for field
- in item.fields
- if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
- )
-
- can_view_live = False
- subsection_units = item.get_children()
- for unit in subsection_units:
- state = compute_unit_state(unit)
- if state == UnitState.public or state == UnitState.draft:
- can_view_live = True
- break
-
- return render_to_response('edit_subsection.html',
- {'subsection': item,
- 'context_course': course,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
- 'lms_link': lms_link,
- 'preview_link': preview_link,
- 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
- 'parent_location': course.location,
- 'parent_item': parent,
- 'policy_metadata': policy_metadata,
- 'subsection_units': subsection_units,
- 'can_view_live': can_view_live
- })
-
-
-@login_required
-def edit_unit(request, location):
- """
- Display an editing page for the specified module.
-
- Expects a GET request with the parameter 'id'.
-
- id: A Location URL
- """
- course = get_course_for_item(location)
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location, depth=1)
-
- lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
-
- component_templates = defaultdict(list)
-
- # Check if there are any advanced modules specified in the course policy. These modules
- # should be specified as a list of strings, where the strings are the names of the modules
- # in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
- course_advanced_keys = course.advanced_modules
-
- # Set component types according to course policy file
- component_types = list(COMPONENT_TYPES)
- if isinstance(course_advanced_keys, list):
- course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
- if len(course_advanced_keys) > 0:
- component_types.append(ADVANCED_COMPONENT_CATEGORY)
- else:
- log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
-
- templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
- for template in templates:
- category = template.location.category
-
- if category in course_advanced_keys:
- category = ADVANCED_COMPONENT_CATEGORY
-
- if category in component_types:
- # This is a hack to create categories for different xmodules
- component_templates[category].append((
- template.display_name_with_default,
- template.location.url(),
- hasattr(template, 'markdown') and template.markdown is not None,
- template.cms.empty,
- ))
-
- components = [
- component.location.url()
- for component
- in item.get_children()
- ]
-
- # TODO (cpennington): If we share units between courses,
- # this will need to change to check permissions correctly so as
- # to pick the correct parent subsection
-
- containing_subsection_locs = modulestore().get_parent_locations(location, None)
- containing_subsection = modulestore().get_item(containing_subsection_locs[0])
-
- containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
- containing_section = modulestore().get_item(containing_section_locs[0])
-
- # cdodge hack. We're having trouble previewing drafts via jump_to redirect
- # so let's generate the link url here
-
- # need to figure out where this item is in the list of children as the preview will need this
- index = 1
- for child in containing_subsection.get_children():
- if child.location == item.location:
- break
- index = index + 1
-
- preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
- 'preview.' + settings.LMS_BASE)
-
- preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
- preview_lms_base=preview_lms_base,
- lms_base=settings.LMS_BASE,
- org=course.location.org,
- course=course.location.course,
- course_name=course.location.name,
- section=containing_section.location.name,
- subsection=containing_subsection.location.name,
- index=index)
-
- unit_state = compute_unit_state(item)
-
- return render_to_response('unit.html', {
- 'context_course': course,
- 'active_tab': 'courseware',
- 'unit': item,
- 'unit_location': location,
- 'components': components,
- 'component_templates': component_templates,
- 'draft_preview_link': preview_lms_link,
- 'published_preview_link': lms_link,
- 'subsection': containing_subsection,
- 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
- 'section': containing_section,
- 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
- 'unit_state': unit_state,
- 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
- })
-
-
-@login_required
-def preview_component(request, location):
- # TODO (vshnayder): change name from id to location in coffee+html as well.
- if not has_access(request.user, location):
- raise HttpResponseForbidden()
-
- component = modulestore().get_item(location)
-
- return render_to_response('component.html', {
- 'preview': get_module_previews(request, component)[0],
- 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def assignment_type_update(request, org, course, category, name):
- '''
- CRUD operations on assignment types for sections and subsections and anything else gradable.
- '''
- location = Location(['i4x', org, course, category, name])
- if not has_access(request.user, location):
- raise HttpResponseForbidden()
-
- if request.method == 'GET':
- return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
- mimetype="application/json")
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
- mimetype="application/json")
-
-
-def user_author_string(user):
- '''Get an author string for commits by this user. Format:
- first last .
-
- If the first and last names are blank, uses the username instead.
- Assumes that the email is not blank.
- '''
- f = user.first_name
- l = user.last_name
- if f == '' and l == '':
- f = user.username
- return '{first} {last} <{email}>'.format(first=f,
- last=l,
- email=user.email)
-
-
-@login_required
-def preview_dispatch(request, preview_id, location, dispatch=None):
- """
- Dispatch an AJAX action to a preview XModule
-
- Expects a POST request, and passes the arguments to the module
-
- preview_id (str): An identifier specifying which preview this module is used for
- location: The Location of the module to dispatch to
- dispatch: The action to execute
- """
-
- descriptor = modulestore().get_item(location)
- instance = load_preview_module(request, preview_id, descriptor)
- # Let the module handle the AJAX
- try:
- ajax_return = instance.handle_ajax(dispatch, request.POST)
-
- except NotFoundError:
- log.exception("Module indicating to user that request doesn't exist")
- raise Http404
-
- except ProcessingError:
- log.warning("Module raised an error while processing AJAX request",
- exc_info=True)
- return HttpResponseBadRequest()
-
- except:
- log.exception("error processing ajax call")
- raise
-
- return HttpResponse(ajax_return)
-
-
-def render_from_lms(template_name, dictionary, context=None, namespace='main'):
- """
- Render a template using the LMS MAKO_TEMPLATES
- """
- return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
-
-
-class SessionKeyValueStore(KeyValueStore):
- def __init__(self, request, model_data):
- self._model_data = model_data
- self._session = request.session
-
- def get(self, key):
- try:
- return self._model_data[key.field_name]
- except (KeyError, InvalidScopeError):
- return self._session[tuple(key)]
-
- def set(self, key, value):
- try:
- self._model_data[key.field_name] = value
- except (KeyError, InvalidScopeError):
- self._session[tuple(key)] = value
-
- def delete(self, key):
- try:
- del self._model_data[key.field_name]
- except (KeyError, InvalidScopeError):
- del self._session[tuple(key)]
-
- def has(self, key):
- return key in self._model_data or key in self._session
-
-
-def preview_module_system(request, preview_id, descriptor):
- """
- Returns a ModuleSystem for the specified descriptor that is specialized for
- rendering module previews.
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- descriptor: An XModuleDescriptor
- """
-
- def preview_model_data(descriptor):
- return DbModel(
- SessionKeyValueStore(request, descriptor._model_data),
- descriptor.module_class,
- preview_id,
- MongoUsage(preview_id, descriptor.location.url()),
- )
-
- return ModuleSystem(
- ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
- # TODO (cpennington): Do we want to track how instructors are using the preview problems?
- track_function=lambda type, event: None,
- filestore=descriptor.system.resources_fs,
- get_module=partial(get_preview_module, request, preview_id),
- render_template=render_from_lms,
- debug=True,
- replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
- user=request.user,
- xblock_model_data=preview_model_data,
- )
-
-
-def get_preview_module(request, preview_id, descriptor):
- """
- Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
- from the set of preview data for the descriptor specified by Location
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- location: A Location
- """
-
- return load_preview_module(request, preview_id, descriptor)
-
-
-def load_preview_module(request, preview_id, descriptor):
- """
- Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
-
- request: The active django request
- preview_id (str): An identifier specifying which preview this module is used for
- descriptor: An XModuleDescriptor
- instance_state: An instance state string
- shared_state: A shared state string
- """
- system = preview_module_system(request, preview_id, descriptor)
- try:
- module = descriptor.xmodule(system)
- except:
- log.debug("Unable to load preview module", exc_info=True)
- module = ErrorDescriptor.from_descriptor(
- descriptor,
- error_msg=exc_info_to_str(sys.exc_info())
- ).xmodule(system)
-
- # cdodge: Special case
- if module.location.category == 'static_tab':
- module.get_html = wrap_xmodule(
- module.get_html,
- module,
- "xmodule_tab_display.html",
- )
- else:
- module.get_html = wrap_xmodule(
- module.get_html,
- module,
- "xmodule_display.html",
- )
-
- module.get_html = replace_static_urls(
- module.get_html,
- getattr(module, 'data_dir', module.location.course),
- course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
- )
-
- return module
-
-
-def get_module_previews(request, descriptor):
- """
- Returns a list of preview XModule html contents. One preview is returned for each
- pair of states returned by get_sample_state() for the supplied descriptor.
-
- descriptor: An XModuleDescriptor
- """
- preview_html = []
- for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
- module = load_preview_module(request, str(idx), descriptor)
- preview_html.append(module.get_html())
- return preview_html
-
-
-def _xmodule_recurse(item, action):
- for child in item.get_children():
- _xmodule_recurse(child, action)
-
- action(item)
-
-
-@login_required
-@expect_json
-def delete_item(request):
- item_location = request.POST['id']
- item_loc = Location(item_location)
-
- # check permissions for this user within this course
- if not has_access(request.user, item_location):
- raise PermissionDenied()
-
- # optional parameter to delete all children (default False)
- delete_children = request.POST.get('delete_children', False)
- delete_all_versions = request.POST.get('delete_all_versions', False)
-
- store = modulestore()
-
- item = store.get_item(item_location)
-
- if delete_children:
- _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
- else:
- store.delete_item(item.location, delete_all_versions)
-
- # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
- if delete_all_versions:
- parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
-
- for parent_loc in parent_locs:
- parent = modulestore('direct').get_item(parent_loc)
- item_url = item_loc.url()
- if item_url in parent.children:
- children = parent.children
- children.remove(item_url)
- parent.children = children
- modulestore('direct').update_children(parent.location, parent.children)
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def save_item(request):
- item_location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, item_location):
- raise PermissionDenied()
-
- store = get_modulestore(Location(item_location))
-
- if request.POST.get('data') is not None:
- data = request.POST['data']
- store.update_item(item_location, data)
-
- # cdodge: note calling request.POST.get('children') will return None if children is an empty array
- # so it lead to a bug whereby the last component to be deleted in the UI was not actually
- # deleting the children object from the children collection
- if 'children' in request.POST and request.POST['children'] is not None:
- children = request.POST['children']
- store.update_children(item_location, children)
-
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if request.POST.get('metadata') is not None:
- posted_metadata = request.POST['metadata']
- # fetch original
- existing_item = modulestore().get_item(item_location)
-
- # update existing metadata with submitted metadata (which can be partial)
- # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key, value in posted_metadata.items():
-
- if posted_metadata[metadata_key] is None:
- # remove both from passed in collection as well as the collection read in from the modulestore
- if metadata_key in existing_item._model_data:
- del existing_item._model_data[metadata_key]
- del posted_metadata[metadata_key]
- else:
- existing_item._model_data[metadata_key] = value
-
- # commit to datastore
- # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- store.update_metadata(item_location, own_metadata(existing_item))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def create_draft(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- # This clones the existing item location to a draft location (the draft is implicit,
- # because modulestore is a Draft modulestore)
- modulestore().clone_item(location, location)
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def publish_draft(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location)
- _xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def unpublish_unit(request):
- location = request.POST['id']
-
- # check permissions for this user within this course
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- item = modulestore().get_item(location)
- _xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
-
- return HttpResponse()
-
-
-@login_required
-@expect_json
-def clone_item(request):
- parent_location = Location(request.POST['parent_location'])
- template = Location(request.POST['template'])
-
- display_name = request.POST.get('display_name')
-
- if not has_access(request.user, parent_location):
- raise PermissionDenied()
-
- parent = get_modulestore(template).get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
-
- new_item = get_modulestore(template).clone_item(template, dest_location)
-
- # replace the display name with an optional parameter passed in from the caller
- if display_name is not None:
- new_item.display_name = display_name
-
- get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
-
- if new_item.location.category not in DETACHED_CATEGORIES:
- get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
-
- return HttpResponse(json.dumps({'id': dest_location.url()}))
-
-
-def upload_asset(request, org, course, coursename):
- '''
- cdodge: this method allows for POST uploading of files into the course asset library, which will
- be supported by GridFS in MongoDB.
- '''
- if request.method != 'POST':
- # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
- return HttpResponseBadRequest()
-
- # construct a location from the passed in path
- location = get_location_and_verify_access(request, org, course, coursename)
-
- # Does the course actually exist?!? Get anything from it to prove its existance
-
- try:
- modulestore().get_item(location)
- except:
- # no return it as a Bad Request response
- logging.error('Could not find course' + location)
- return HttpResponseBadRequest()
-
- # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
- # nomenclature since we're using a FileSystem paradigm here. We're just imposing
- # the Location string formatting expectations to keep things a bit more consistent
-
- filename = request.FILES['file'].name
- mime_type = request.FILES['file'].content_type
- filedata = request.FILES['file'].read()
-
- content_loc = StaticContent.compute_location(org, course, filename)
- content = StaticContent(content_loc, filename, mime_type, filedata)
-
- # first let's see if a thumbnail can be created
- (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
-
- # delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
- del_cached_content(thumbnail_location)
- # now store thumbnail location only if we could create it
- if thumbnail_content is not None:
- content.thumbnail_location = thumbnail_location
-
- # then commit the content
- contentstore().save(content)
- del_cached_content(content.location)
-
- # readback the saved content - we need the database timestamp
- readback = contentstore().find(content.location)
-
- response_payload = {'displayname': content.name,
- 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
- 'url': StaticContent.get_url_path_from_location(content.location),
- 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
- 'msg': 'Upload completed'
- }
-
- response = HttpResponse(json.dumps(response_payload))
- response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
- return response
-
-
-@login_required
-@ensure_csrf_cookie
-def manage_users(request, location):
- '''
- This view will return all CMS users who are editors for the specified course
- '''
- # check that logged in user has permissions to this item
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
- raise PermissionDenied()
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('manage_users.html', {
- 'active_tab': 'users',
- 'context_course': course_module,
- 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
- 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
- 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
- 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
- 'request_user_id': request.user.id
- })
-
-
-def create_json_response(errmsg=None):
- if errmsg is not None:
- resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
- else:
- resp = HttpResponse(json.dumps({'Status': 'OK'}))
-
- return resp
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def add_user(request, location):
- '''
- This POST-back view will add a user - specified by email - to the list of editors for
- the specified course
- '''
- email = request.POST["email"]
-
- if email == '':
- return create_json_response('Please specify an email address.')
-
- # check that logged in user has admin permissions to this course
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
- raise PermissionDenied()
-
- user = get_user_by_email(email)
-
- # user doesn't exist?!? Return error.
- if user is None:
- return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
-
- # user exists, but hasn't activated account?!?
- if not user.is_active:
- return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
-
- # ok, we're cool to add to the course group
- add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
-
- return create_json_response()
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def remove_user(request, location):
- '''
- This POST-back view will remove a user - specified by email - from the list of editors for
- the specified course
- '''
-
- email = request.POST["email"]
-
- # check that logged in user has admin permissions on this course
- if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
- raise PermissionDenied()
-
- user = get_user_by_email(email)
- if user is None:
- return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
-
- # make sure we're not removing ourselves
- if user.id == request.user.id:
- raise PermissionDenied()
-
- remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
-
- return create_json_response()
-
-
-# points to the temporary course landing page with log in and sign up
-def landing(request, org, course, coursename):
- return render_to_response('temp-course-landing.html', {})
-
-
-@login_required
-@ensure_csrf_cookie
-def static_pages(request, org, course, coursename):
-
- location = get_location_and_verify_access(request, org, course, coursename)
-
- course = modulestore().get_item(location)
-
- return render_to_response('static-pages.html', {
- 'active_tab': 'pages',
- 'context_course': course,
- })
-
-
-def edit_static(request, org, course, coursename):
- return render_to_response('edit-static-page.html', {})
-
-
-@login_required
-@expect_json
-def reorder_static_tabs(request):
- tabs = request.POST['tabs']
- course = get_course_for_item(tabs[0])
-
- if not has_access(request.user, course.location):
- raise PermissionDenied()
-
- # get list of existing static tabs in course
- # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
- # that we know about) otherwise we can drop some!
-
- existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
- if len(existing_static_tabs) != len(tabs):
- return HttpResponseBadRequest()
-
- # load all reference tabs, return BadRequest if we can't find any of them
- tab_items = []
- for tab in tabs:
- item = modulestore('direct').get_item(Location(tab))
- if item is None:
- return HttpResponseBadRequest()
-
- tab_items.append(item)
-
- # now just go through the existing course_tabs and re-order the static tabs
- reordered_tabs = []
- static_tab_idx = 0
- for tab in course.tabs:
- if tab['type'] == 'static_tab':
- reordered_tabs.append({'type': 'static_tab',
- 'name': tab_items[static_tab_idx].display_name,
- 'url_slug': tab_items[static_tab_idx].location.name})
- static_tab_idx += 1
- else:
- reordered_tabs.append(tab)
-
- # OK, re-assemble the static tabs in the new order
- course.tabs = reordered_tabs
- modulestore('direct').update_metadata(course.location, own_metadata(course))
- return HttpResponse()
-
-
-@login_required
-@ensure_csrf_cookie
-def edit_tabs(request, org, course, coursename):
- location = ['i4x', org, course, 'course', coursename]
- course_item = modulestore().get_item(location)
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- # see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
- if course_item.tabs is None or len(course_item.tabs) == 0:
- initialize_course_tabs(course_item)
-
- # first get all static tabs from the tabs list
- # we do this because this is also the order in which items are displayed in the LMS
- static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
-
- static_tabs = []
- for static_tab_ref in static_tabs_refs:
- static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
- static_tabs.append(modulestore('direct').get_item(static_tab_loc))
-
- components = [
- static_tab.location.url()
- for static_tab
- in static_tabs
- ]
-
- return render_to_response('edit-tabs.html', {
- 'active_tab': 'pages',
- 'context_course': course_item,
- 'components': components
- })
-
-
-def not_found(request):
- return render_to_response('error.html', {'error': '404'})
-
-
-def server_error(request):
- return render_to_response('error.html', {'error': '500'})
-
-
-@login_required
-@ensure_csrf_cookie
-def course_info(request, org, course, name, provided_id=None):
- """
- Send models and views as well as html for editing the course info to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- # get current updates
- location = ['i4x', org, course, 'course_info', "updates"]
-
- return render_to_response('course_info.html', {
- 'active_tab': 'courseinfo-tab',
- 'context_course': course_module,
- 'url_base': "/" + org + "/" + course + "/",
- 'course_updates': json.dumps(get_course_updates(location)),
- 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_info_updates(request, org, course, provided_id=None):
- """
- restful CRUD operations on course_info updates.
-
- org, course: Attributes of the Location for the item to edit
- provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
- """
- # ??? No way to check for access permission afaik
- # get current updates
- location = ['i4x', org, course, 'course_info', "updates"]
-
- # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
- # Possibly due to my removing the seemingly redundant pattern in urls.py
- if provided_id == '':
- provided_id = None
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- real_method = get_request_method(request)
-
- if request.method == 'GET':
- return HttpResponse(json.dumps(get_course_updates(location)),
- mimetype="application/json")
- elif real_method == 'DELETE':
- try:
- return HttpResponse(json.dumps(delete_course_update(location,
- request.POST, provided_id)), mimetype="application/json")
- except:
- return HttpResponseBadRequest("Failed to delete",
- content_type="text/plain")
- elif request.method == 'POST':
- try:
- return HttpResponse(json.dumps(update_course_updates(location,
- request.POST, provided_id)), mimetype="application/json")
- except:
- return HttpResponseBadRequest("Failed to save",
- content_type="text/plain")
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def module_info(request, module_location):
- location = Location(module_location)
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- real_method = get_request_method(request)
-
- rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
- logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- if real_method == 'GET':
- return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
- elif real_method == 'POST' or real_method == 'PUT':
- return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
- else:
- return HttpResponseBadRequest()
-
-
-@login_required
-@ensure_csrf_cookie
-def get_course_settings(request, org, course, name):
- """
- Send models and views as well as html for editing the course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('settings.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'details_url': reverse(course_settings_updates,
- kwargs={"org": org,
- "course": course,
- "name": name,
- "section": "details"})
- })
-
-
-@login_required
-@ensure_csrf_cookie
-def course_config_graders_page(request, org, course, name):
- """
- Send models and views as well as html for editing the course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
- course_details = CourseGradingModel.fetch(location)
-
- return render_to_response('settings_graders.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
- })
-
-
-@login_required
-@ensure_csrf_cookie
-def course_config_advanced_page(request, org, course, name):
- """
- Send models and views as well as html for editing the advanced course settings to the client.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('settings_advanced.html', {
- 'context_course': course_module,
- 'course_location': location,
- 'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
- })
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_settings_updates(request, org, course, name, section):
- """
- restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
- through json (not rendering any html) and handles section level operations rather than whole page.
-
- org, course: Attributes of the Location for the item to edit
- section: one of details, faculty, grading, problems, discussions
- """
- get_location_and_verify_access(request, org, course, name)
-
- if section == 'details':
- manager = CourseDetails
- elif section == 'grading':
- manager = CourseGradingModel
- else:
- return
-
- if request.method == 'GET':
- # Cannot just do a get w/o knowing the course name :-(
- return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
- mimetype="application/json")
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
- mimetype="application/json")
-
-
-@expect_json
-@login_required
-@ensure_csrf_cookie
-def course_grader_updates(request, org, course, name, grader_index=None):
- """
- restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
- through json (not rendering any html) and handles section level operations rather than whole page.
-
- org, course: Attributes of the Location for the item to edit
- """
-
- location = get_location_and_verify_access(request, org, course, name)
-
- real_method = get_request_method(request)
-
- if real_method == 'GET':
- # Cannot just do a get w/o knowing the course name :-(
- return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
- mimetype="application/json")
- elif real_method == "DELETE":
- # ??? Should this return anything? Perhaps success fail?
- CourseGradingModel.delete_grader(Location(location), grader_index)
- return HttpResponse()
- elif request.method == 'POST': # post or put, doesn't matter.
- return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
- mimetype="application/json")
-
-
-# # NB: expect_json failed on ["key", "key2"] and json payload
-@login_required
-@ensure_csrf_cookie
-def course_advanced_updates(request, org, course, name):
- """
- restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
- the payload is either a key or a list of keys to delete.
-
- org, course: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- real_method = get_request_method(request)
-
- if real_method == 'GET':
- return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
- elif real_method == 'DELETE':
- return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
- mimetype="application/json")
- elif real_method == 'POST' or real_method == 'PUT':
- # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
- request_body = json.loads(request.body)
- #Whether or not to filter the tabs key out of the settings metadata
- filter_tabs = True
- #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
- #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
- #module, and to remove it if they have removed the open ended elements.
- if ADVANCED_COMPONENT_POLICY_KEY in request_body:
- #Check to see if the user instantiated any open ended components
- found_oe_type = False
- #Get the course so that we can scrape current tabs
- course_module = modulestore().get_item(location)
- for oe_type in OPEN_ENDED_COMPONENT_TYPES:
- if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
- #Add an open ended tab to the course if needed
- changed, new_tabs = add_open_ended_panel_tab(course_module)
- #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
- if changed:
- request_body.update({'tabs': new_tabs})
- #Indicate that tabs should not be filtered out of the metadata
- filter_tabs = False
- #Set this flag to avoid the open ended tab removal code below.
- found_oe_type = True
- break
- #If we did not find an open ended module type in the advanced settings,
- # we may need to remove the open ended tab from the course.
- if not found_oe_type:
- #Remove open ended tab to the course if needed
- changed, new_tabs = remove_open_ended_panel_tab(course_module)
- if changed:
- request_body.update({'tabs': new_tabs})
- #Indicate that tabs should not be filtered out of the metadata
- filter_tabs = False
- response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
- return HttpResponse(response_json, mimetype="application/json")
-
-
-@ensure_csrf_cookie
-@login_required
-def get_checklists(request, org, course, name):
- """
- Send models, views, and html for displaying the course checklists.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- modulestore = get_modulestore(location)
- course_module = modulestore.get_item(location)
- new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
- template_module = modulestore.get_item(new_course_template)
-
- # If course was created before checklists were introduced, copy them over from the template.
- copied = False
- if not course_module.checklists:
- course_module.checklists = template_module.checklists
- copied = True
-
- checklists, modified = expand_checklist_action_urls(course_module)
- if copied or modified:
- modulestore.update_metadata(location, own_metadata(course_module))
- return render_to_response('checklists.html',
- {
- 'context_course': course_module,
- 'checklists': checklists
- })
-
-
-@ensure_csrf_cookie
-@login_required
-def update_checklist(request, org, course, name, checklist_index=None):
- """
- restful CRUD operations on course checklists. The payload is a json rep of
- the modified checklist. For PUT or POST requests, the index of the
- checklist being modified must be included; the returned payload will
- be just that one checklist. For GET requests, the returned payload
- is a json representation of the list of all checklists.
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
- modulestore = get_modulestore(location)
- course_module = modulestore.get_item(location)
-
- real_method = get_request_method(request)
- if real_method == 'POST' or real_method == 'PUT':
- if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
- index = int(checklist_index)
- course_module.checklists[index] = json.loads(request.body)
- checklists, modified = expand_checklist_action_urls(course_module)
- modulestore.update_metadata(location, own_metadata(course_module))
- return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
- else:
- return HttpResponseBadRequest(
- "Could not save checklist state because the checklist index was out of range or unspecified.",
- content_type="text/plain")
- elif request.method == 'GET':
- # In the JavaScript view initialize method, we do a fetch to get all the checklists.
- checklists, modified = expand_checklist_action_urls(course_module)
- if modified:
- modulestore.update_metadata(location, own_metadata(course_module))
- return HttpResponse(json.dumps(checklists), mimetype="application/json")
- else:
- return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
-
-
-def expand_checklist_action_urls(course_module):
- """
- Gets the checklists out of the course module and expands their action urls
- if they have not yet been expanded.
-
- Returns the checklists with modified urls, as well as a boolean
- indicating whether or not the checklists were modified.
- """
- checklists = course_module.checklists
- modified = False
- for checklist in checklists:
- if not checklist.get('action_urls_expanded', False):
- for item in checklist.get('items'):
- item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
- checklist['action_urls_expanded'] = True
- modified = True
-
- return checklists, modified
-
-
-@login_required
-@ensure_csrf_cookie
-def asset_index(request, org, course, name):
- """
- Display an editable asset library
-
- org, course, name: Attributes of the Location for the item to edit
- """
- location = get_location_and_verify_access(request, org, course, name)
-
- upload_asset_callback_url = reverse('upload_asset', kwargs={
- 'org': org,
- 'course': course,
- 'coursename': name
- })
-
- course_module = modulestore().get_item(location)
-
- course_reference = StaticContent.compute_location(org, course, name)
- assets = contentstore().get_all_content_for_course(course_reference)
-
- # sort in reverse upload date order
- assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
-
- asset_display = []
- for asset in assets:
- id = asset['_id']
- display_info = {}
- display_info['displayname'] = asset['displayname']
- display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
-
- asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
- display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
-
- # note, due to the schema change we may not have a 'thumbnail_location' in the result set
- _thumbnail_location = asset.get('thumbnail_location', None)
- thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
- display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
-
- asset_display.append(display_info)
-
- return render_to_response('asset_index.html', {
- 'active_tab': 'assets',
- 'context_course': course_module,
- 'assets': asset_display,
- 'upload_asset_callback_url': upload_asset_callback_url
- })
-
-
-# points to the temporary edge page
-def edge(request):
- return render_to_response('university_profiles/edge.html', {})
-
-
-@login_required
-@expect_json
-def create_new_course(request):
-
- if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
- raise PermissionDenied()
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- # TODO: write a test that creates two courses, one with the factory and
- # the other with this method, then compare them to make sure they are
- # equivalent.
- template = Location(request.POST['template'])
- org = request.POST.get('org')
- number = request.POST.get('number')
- display_name = request.POST.get('display_name')
-
- try:
- dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
- except InvalidLocationError as e:
- return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
-
- # see if the course already exists
- existing_course = None
- try:
- existing_course = modulestore('direct').get_item(dest_location)
- except ItemNotFoundError:
- pass
-
- if existing_course is not None:
- 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)
-
- # clone a default 'about' module as well
-
- about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
- dest_about_location = dest_location._replace(category='about', name='overview')
- modulestore('direct').clone_item(about_template_location, dest_about_location)
-
- if display_name is not None:
- new_course.display_name = display_name
-
- # set a default start date to now
- new_course.start = time.gmtime()
-
- initialize_course_tabs(new_course)
-
- create_all_course_groups(request.user, new_course.location)
-
- return HttpResponse(json.dumps({'id': new_course.location.url()}))
-
-
-def initialize_course_tabs(course):
- # set up the default tabs
- # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
- # at least a list populated with the minimal times
- # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
- # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
-
-
-@ensure_csrf_cookie
-@login_required
-def import_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- if request.method == 'POST':
- filename = request.FILES['course-data'].name
-
- if not filename.endswith('.tar.gz'):
- return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
-
- data_root = path(settings.GITHUB_REPO_ROOT)
-
- course_subdir = "{0}-{1}-{2}".format(org, course, name)
- course_dir = data_root / course_subdir
- if not course_dir.isdir():
- os.mkdir(course_dir)
-
- temp_filepath = course_dir / filename
-
- logging.debug('importing course to {0}'.format(temp_filepath))
-
- # stream out the uploaded files in chunks to disk
- temp_file = open(temp_filepath, 'wb+')
- for chunk in request.FILES['course-data'].chunks():
- temp_file.write(chunk)
- temp_file.close()
-
- tf = tarfile.open(temp_filepath)
- tf.extractall(course_dir + '/')
-
- # 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
-
- 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)
-
- module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_subdir], load_error_modules=False,
- static_content_store=contentstore(),
- target_location_namespace=Location(location),
- draft_store=modulestore())
-
- # 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))
-
- create_all_course_groups(request.user, course_items[0].location)
-
- return HttpResponse(json.dumps({'Status': 'OK'}))
- else:
- course_module = modulestore().get_item(location)
-
- return render_to_response('import.html', {
- 'context_course': course_module,
- 'active_tab': 'import',
- 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
- })
-
-
-@ensure_csrf_cookie
-@login_required
-def generate_export_course(request, org, course, name):
- location = get_location_and_verify_access(request, org, course, name)
-
- loc = Location(location)
- export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
-
- root_dir = path(mkdtemp())
-
- # export out to a tempdir
-
- logging.debug('root = {0}'.format(root_dir))
-
- export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
- #filename = root_dir / name + '.tar.gz'
-
- logging.debug('tar file being generated at {0}'.format(export_file.name))
- tf = tarfile.open(name=export_file.name, mode='w:gz')
- tf.add(root_dir / name, arcname=name)
- tf.close()
-
- # remove temp dir
- shutil.rmtree(root_dir / name)
-
- wrapper = FileWrapper(export_file)
- response = HttpResponse(wrapper, content_type='application/x-tgz')
- response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
- response['Content-Length'] = os.path.getsize(export_file.name)
- return response
-
-
-@ensure_csrf_cookie
-@login_required
-def export_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- course_module = modulestore().get_item(location)
-
- return render_to_response('export.html', {
- 'context_course': course_module,
- 'active_tab': 'export',
- 'successful_import_redirect_url': ''
- })
-
-
-def event(request):
- '''
- A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
- console logs don't get distracted :-)
- '''
- return HttpResponse(True)
-
-
-def render_404(request):
- return HttpResponseNotFound(render_to_string('404.html', {}))
-
-
-def render_500(request):
- return HttpResponseServerError(render_to_string('500.html', {}))
-
-
-def get_location_and_verify_access(request, org, course, name):
- """
- Create the location tuple verify that the user has permissions
- to view the location. Returns the location.
- """
- location = ['i4x', org, course, 'course', name]
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- return location
-
-
-def get_request_method(request):
- """
- Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
- what type of request came from the client, and return it.
- """
- # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
- if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
- real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
- else:
- real_method = request.method
-
- return real_method
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
new file mode 100644
index 0000000000..0b7c271b1e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -0,0 +1,17 @@
+from new import *
+from error import *
+from course import *
+from item import *
+from public import *
+from user import *
+from preview import *
+from assets import *
+from checklist import *
+from requests import landing
+
+
+"""
+
+from main import *
+
+"""
diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py
new file mode 100644
index 0000000000..dd3add1099
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/access.py
@@ -0,0 +1,39 @@
+#from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
+#from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
+#from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
+
+from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
+from auth.authz import is_user_in_course_group_role
+from contentstore.utils import get_course_location_for_item
+
+
+def get_location_and_verify_access(request, org, course, name):
+ """
+ Create the location tuple verify that the user has permissions
+ to view the location. Returns the location.
+ """
+ location = ['i4x', org, course, 'course', name]
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ return location
+
+
+def has_access(user, location, role=STAFF_ROLE_NAME):
+ '''
+ Return True if user allowed to access this piece of data
+ Note that the CMS permissions model is with respect to courses
+ There is a super-admin permissions if user.is_staff is set
+ Also, since we're unifying the user database between LMS and CAS,
+ I'm presuming that the course instructor (formally known as admin)
+ will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our queries here as INSTRUCTOR
+ has all the rights that STAFF do
+ '''
+ course_location = get_course_location_for_item(location)
+ _has_access = is_user_in_course_group_role(user, course_location, role)
+ # if we're not in STAFF, perhaps we're in INSTRUCTOR groups
+ if not _has_access and role == STAFF_ROLE_NAME:
+ _has_access = is_user_in_course_group_role(user, course_location, INSTRUCTOR_ROLE_NAME)
+ return _has_access
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
new file mode 100644
index 0000000000..616b04342d
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -0,0 +1,118 @@
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from xmodule.contentstore.content import StaticContent
+from access import get_location_and_verify_access
+from xmodule.util.date_utils import get_default_time_display
+from mitxmako.shortcuts import render_to_response
+
+
+@login_required
+@ensure_csrf_cookie
+def asset_index(request, org, course, name):
+ """
+ Display an editable asset library
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ upload_asset_callback_url = reverse('upload_asset', kwargs={
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
+
+ course_module = modulestore().get_item(location)
+
+ course_reference = StaticContent.compute_location(org, course, name)
+ assets = contentstore().get_all_content_for_course(course_reference)
+
+ # sort in reverse upload date order
+ assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
+
+ asset_display = []
+ for asset in assets:
+ id = asset['_id']
+ display_info = {}
+ display_info['displayname'] = asset['displayname']
+ display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
+
+ asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
+ display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
+
+ # note, due to the schema change we may not have a 'thumbnail_location' in the result set
+ _thumbnail_location = asset.get('thumbnail_location', None)
+ thumbnail_location = Location(_thumbnail_location) if _thumbnail_location is not None else None
+ display_info['thumb_url'] = StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_location is not None else None
+
+ asset_display.append(display_info)
+
+ return render_to_response('asset_index.html', {
+ 'active_tab': 'assets',
+ 'context_course': course_module,
+ 'assets': asset_display,
+ 'upload_asset_callback_url': upload_asset_callback_url
+ })
+
+
+def upload_asset(request, org, course, coursename):
+ '''
+ cdodge: this method allows for POST uploading of files into the course asset library, which will
+ be supported by GridFS in MongoDB.
+ '''
+ if request.method != 'POST':
+ # (cdodge) @todo: Is there a way to do a - say - 'raise Http400'?
+ return HttpResponseBadRequest()
+
+ # construct a location from the passed in path
+ location = get_location_and_verify_access(request, org, course, coursename)
+
+ # Does the course actually exist?!? Get anything from it to prove its existance
+
+ try:
+ modulestore().get_item(location)
+ except:
+ # no return it as a Bad Request response
+ logging.error('Could not find course' + location)
+ return HttpResponseBadRequest()
+
+ # compute a 'filename' which is similar to the location formatting, we're using the 'filename'
+ # nomenclature since we're using a FileSystem paradigm here. We're just imposing
+ # the Location string formatting expectations to keep things a bit more consistent
+
+ filename = request.FILES['file'].name
+ mime_type = request.FILES['file'].content_type
+ filedata = request.FILES['file'].read()
+
+ content_loc = StaticContent.compute_location(org, course, filename)
+ content = StaticContent(content_loc, filename, mime_type, filedata)
+
+ # first let's see if a thumbnail can be created
+ (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content)
+
+ # delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
+ del_cached_content(thumbnail_location)
+ # now store thumbnail location only if we could create it
+ if thumbnail_content is not None:
+ content.thumbnail_location = thumbnail_location
+
+ # then commit the content
+ contentstore().save(content)
+ del_cached_content(content.location)
+
+ # readback the saved content - we need the database timestamp
+ readback = contentstore().find(content.location)
+
+ response_payload = {'displayname': content.name,
+ 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
+ 'url': StaticContent.get_url_path_from_location(content.location),
+ 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
+ 'msg': 'Upload completed'
+ }
+
+ response = HttpResponse(json.dumps(response_payload))
+ response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
+ return response
+
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
new file mode 100644
index 0000000000..376e041523
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -0,0 +1,98 @@
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+from access import get_location_and_verify_access
+from mitxmako.shortcuts import render_to_response
+from contentstore.utils import get_modulestore, get_url_reverse
+
+@ensure_csrf_cookie
+@login_required
+def get_checklists(request, org, course, name):
+ """
+ Send models, views, and html for displaying the course checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+ new_course_template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
+ template_module = modulestore.get_item(new_course_template)
+
+ # If course was created before checklists were introduced, copy them over from the template.
+ copied = False
+ if not course_module.checklists:
+ course_module.checklists = template_module.checklists
+ copied = True
+
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if copied or modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return render_to_response('checklists.html',
+ {
+ 'context_course': course_module,
+ 'checklists': checklists
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def update_checklist(request, org, course, name, checklist_index=None):
+ """
+ restful CRUD operations on course checklists. The payload is a json rep of
+ the modified checklist. For PUT or POST requests, the index of the
+ checklist being modified must be included; the returned payload will
+ be just that one checklist. For GET requests, the returned payload
+ is a json representation of the list of all checklists.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+ modulestore = get_modulestore(location)
+ course_module = modulestore.get_item(location)
+
+ real_method = get_request_method(request)
+ if real_method == 'POST' or real_method == 'PUT':
+ if checklist_index is not None and 0 <= int(checklist_index) < len(course_module.checklists):
+ index = int(checklist_index)
+ course_module.checklists[index] = json.loads(request.body)
+ checklists, modified = expand_checklist_action_urls(course_module)
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists[index]), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest(
+ "Could not save checklist state because the checklist index was out of range or unspecified.",
+ content_type="text/plain")
+ elif request.method == 'GET':
+ # In the JavaScript view initialize method, we do a fetch to get all the checklists.
+ checklists, modified = expand_checklist_action_urls(course_module)
+ if modified:
+ modulestore.update_metadata(location, own_metadata(course_module))
+ return HttpResponse(json.dumps(checklists), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest("Unsupported request.", content_type="text/plain")
+
+
+def expand_checklist_action_urls(course_module):
+ """
+ Gets the checklists out of the course module and expands their action urls
+ if they have not yet been expanded.
+
+ Returns the checklists with modified urls, as well as a boolean
+ indicating whether or not the checklists were modified.
+ """
+ checklists = course_module.checklists
+ modified = False
+ for checklist in checklists:
+ if not checklist.get('action_urls_expanded', False):
+ for item in checklist.get('items'):
+ item['action_url'] = get_url_reverse(item.get('action_url'), course_module)
+ checklist['action_urls_expanded'] = True
+ modified = True
+
+ return checklists, modified
+
+
+
+
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
new file mode 100644
index 0000000000..fc2214f970
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -0,0 +1,202 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+@login_required
+@expect_json
+def create_new_course(request):
+
+ if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
+ raise PermissionDenied()
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ # TODO: write a test that creates two courses, one with the factory and
+ # the other with this method, then compare them to make sure they are
+ # equivalent.
+ template = Location(request.POST['template'])
+ org = request.POST.get('org')
+ number = request.POST.get('number')
+ display_name = request.POST.get('display_name')
+
+ try:
+ dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
+ except InvalidLocationError as e:
+ return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
+
+ # see if the course already exists
+ existing_course = None
+ try:
+ existing_course = modulestore('direct').get_item(dest_location)
+ except ItemNotFoundError:
+ pass
+
+ if existing_course is not None:
+ 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)
+
+ # clone a default 'about' module as well
+
+ about_template_location = Location(['i4x', 'edx', 'templates', 'about', 'overview'])
+ dest_about_location = dest_location._replace(category='about', name='overview')
+ modulestore('direct').clone_item(about_template_location, dest_about_location)
+
+ if display_name is not None:
+ new_course.display_name = display_name
+
+ # set a default start date to now
+ new_course.start = time.gmtime()
+
+ initialize_course_tabs(new_course)
+
+ create_all_course_groups(request.user, new_course.location)
+
+ return HttpResponse(json.dumps({'id': new_course.location.url()}))
+
+
+def initialize_course_tabs(course):
+ # set up the default tabs
+ # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
+ # at least a list populated with the minimal times
+ # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
+ # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
+
+
+@ensure_csrf_cookie
+@login_required
+def import_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ if request.method == 'POST':
+ filename = request.FILES['course-data'].name
+
+ if not filename.endswith('.tar.gz'):
+ return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
+
+ data_root = path(settings.GITHUB_REPO_ROOT)
+
+ course_subdir = "{0}-{1}-{2}".format(org, course, name)
+ course_dir = data_root / course_subdir
+ if not course_dir.isdir():
+ os.mkdir(course_dir)
+
+ temp_filepath = course_dir / filename
+
+ logging.debug('importing course to {0}'.format(temp_filepath))
+
+ # stream out the uploaded files in chunks to disk
+ temp_file = open(temp_filepath, 'wb+')
+ for chunk in request.FILES['course-data'].chunks():
+ temp_file.write(chunk)
+ temp_file.close()
+
+ tf = tarfile.open(temp_filepath)
+ tf.extractall(course_dir + '/')
+
+ # 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
+
+ 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)
+
+ module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
+ [course_subdir], load_error_modules=False,
+ static_content_store=contentstore(),
+ target_location_namespace=Location(location),
+ draft_store=modulestore())
+
+ # 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))
+
+ create_all_course_groups(request.user, course_items[0].location)
+
+ return HttpResponse(json.dumps({'Status': 'OK'}))
+ else:
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('import.html', {
+ 'context_course': course_module,
+ 'active_tab': 'import',
+ 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def generate_export_course(request, org, course, name):
+ location = get_location_and_verify_access(request, org, course, name)
+
+ loc = Location(location)
+ export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
+
+ root_dir = path(mkdtemp())
+
+ # export out to a tempdir
+
+ logging.debug('root = {0}'.format(root_dir))
+
+ export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
+ #filename = root_dir / name + '.tar.gz'
+
+ logging.debug('tar file being generated at {0}'.format(export_file.name))
+ tf = tarfile.open(name=export_file.name, mode='w:gz')
+ tf.add(root_dir / name, arcname=name)
+ tf.close()
+
+ # remove temp dir
+ shutil.rmtree(root_dir / name)
+
+ wrapper = FileWrapper(export_file)
+ response = HttpResponse(wrapper, content_type='application/x-tgz')
+ response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
+ response['Content-Length'] = os.path.getsize(export_file.name)
+ return response
+
+@ensure_csrf_cookie
+@login_required
+def export_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('export.html', {
+ 'context_course': course_module,
+ 'active_tab': 'export',
+ 'successful_import_redirect_url': ''
+ })
+
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
new file mode 100644
index 0000000000..527f137b9e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -0,0 +1,21 @@
+from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+
+from mitxmako.shortcuts import render_to_string, render_to_response
+
+
+def not_found(request):
+ return render_to_response('error.html', {'error': '404'})
+
+
+def server_error(request):
+ return render_to_response('error.html', {'error': '500'})
+
+
+def render_404(request):
+ return HttpResponseNotFound(render_to_string('404.html', {}))
+
+
+def render_500(request):
+ return HttpResponseServerError(render_to_string('500.html', {}))
+
+
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
new file mode 100644
index 0000000000..876251203e
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -0,0 +1,123 @@
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+
+@login_required
+@expect_json
+def save_item(request):
+ item_location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, item_location):
+ raise PermissionDenied()
+
+ store = get_modulestore(Location(item_location))
+
+ if request.POST.get('data') is not None:
+ data = request.POST['data']
+ store.update_item(item_location, data)
+
+ # cdodge: note calling request.POST.get('children') will return None if children is an empty array
+ # so it lead to a bug whereby the last component to be deleted in the UI was not actually
+ # deleting the children object from the children collection
+ if 'children' in request.POST and request.POST['children'] is not None:
+ children = request.POST['children']
+ store.update_children(item_location, children)
+
+ # cdodge: also commit any metadata which might have been passed along in the
+ # POST from the client, if it is there
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
+ if request.POST.get('metadata') is not None:
+ posted_metadata = request.POST['metadata']
+ # fetch original
+ existing_item = modulestore().get_item(item_location)
+
+ # update existing metadata with submitted metadata (which can be partial)
+ # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
+ for metadata_key, value in posted_metadata.items():
+
+ if posted_metadata[metadata_key] is None:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ if metadata_key in existing_item._model_data:
+ del existing_item._model_data[metadata_key]
+ del posted_metadata[metadata_key]
+ else:
+ existing_item._model_data[metadata_key] = value
+
+ # commit to datastore
+ # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
+ store.update_metadata(item_location, own_metadata(existing_item))
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def clone_item(request):
+ parent_location = Location(request.POST['parent_location'])
+ template = Location(request.POST['template'])
+
+ display_name = request.POST.get('display_name')
+
+ if not has_access(request.user, parent_location):
+ raise PermissionDenied()
+
+ parent = get_modulestore(template).get_item(parent_location)
+ dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ new_item = get_modulestore(template).clone_item(template, dest_location)
+
+ # replace the display name with an optional parameter passed in from the caller
+ if display_name is not None:
+ new_item.display_name = display_name
+
+ get_modulestore(template).update_metadata(new_item.location.url(), own_metadata(new_item))
+
+ if new_item.location.category not in DETACHED_CATEGORIES:
+ get_modulestore(parent.location).update_children(parent_location, parent.children + [new_item.location.url()])
+
+ return HttpResponse(json.dumps({'id': dest_location.url()}))
+
+
+@login_required
+@expect_json
+def delete_item(request):
+ item_location = request.POST['id']
+ item_loc = Location(item_location)
+
+ # check permissions for this user within this course
+ if not has_access(request.user, item_location):
+ raise PermissionDenied()
+
+ # optional parameter to delete all children (default False)
+ delete_children = request.POST.get('delete_children', False)
+ delete_all_versions = request.POST.get('delete_all_versions', False)
+
+ store = modulestore()
+
+ item = store.get_item(item_location)
+
+ if delete_children:
+ _xmodule_recurse(item, lambda i: store.delete_item(i.location, delete_all_versions))
+ else:
+ store.delete_item(item.location, delete_all_versions)
+
+ # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
+ if delete_all_versions:
+ parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
+
+ for parent_loc in parent_locs:
+ parent = modulestore('direct').get_item(parent_loc)
+ item_url = item_loc.url()
+ if item_url in parent.children:
+ children = parent.children
+ children.remove(item_url)
+ parent.children = children
+ modulestore('direct').update_children(parent.location, parent.children)
+
+ return HttpResponse()
+
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
new file mode 100644
index 0000000000..f473b962c5
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -0,0 +1,156 @@
+from django.contrib.auth.decorators import login_required
+from xblock.runtime import DbModel
+from xmodule.x_module import ModuleSystem
+from xmodule.modulestore.mongo import MongoUsage
+from xmodule_modifiers import wrap_xmodule
+from session_kv_store import SessionKeyValueStore
+from requests import render_from_lms
+from functools import partial
+
+@login_required
+def preview_dispatch(request, preview_id, location, dispatch=None):
+ """
+ Dispatch an AJAX action to a preview XModule
+
+ Expects a POST request, and passes the arguments to the module
+
+ preview_id (str): An identifier specifying which preview this module is used for
+ location: The Location of the module to dispatch to
+ dispatch: The action to execute
+ """
+
+ descriptor = modulestore().get_item(location)
+ instance = load_preview_module(request, preview_id, descriptor)
+ # Let the module handle the AJAX
+ try:
+ ajax_return = instance.handle_ajax(dispatch, request.POST)
+
+ except NotFoundError:
+ log.exception("Module indicating to user that request doesn't exist")
+ raise Http404
+
+ except ProcessingError:
+ log.warning("Module raised an error while processing AJAX request",
+ exc_info=True)
+ return HttpResponseBadRequest()
+
+ except:
+ log.exception("error processing ajax call")
+ raise
+
+ return HttpResponse(ajax_return)
+
+@login_required
+def preview_component(request, location):
+ # TODO (vshnayder): change name from id to location in coffee+html as well.
+ if not has_access(request.user, location):
+ raise HttpResponseForbidden()
+
+ component = modulestore().get_item(location)
+
+ return render_to_response('component.html', {
+ 'preview': get_module_previews(request, component)[0],
+ 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
+ })
+
+
+
+def preview_module_system(request, preview_id, descriptor):
+ """
+ Returns a ModuleSystem for the specified descriptor that is specialized for
+ rendering module previews.
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ descriptor: An XModuleDescriptor
+ """
+
+ def preview_model_data(descriptor):
+ return DbModel(
+ SessionKeyValueStore(request, descriptor._model_data),
+ descriptor.module_class,
+ preview_id,
+ MongoUsage(preview_id, descriptor.location.url()),
+ )
+
+ return ModuleSystem(
+ ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
+ # TODO (cpennington): Do we want to track how instructors are using the preview problems?
+ track_function=lambda type, event: None,
+ filestore=descriptor.system.resources_fs,
+ get_module=partial(get_preview_module, request, preview_id),
+ render_template=render_from_lms,
+ debug=True,
+ replace_urls=partial(static_replace.replace_static_urls, data_directory=None, course_namespace=descriptor.location),
+ user=request.user,
+ xblock_model_data=preview_model_data,
+ )
+
+def get_preview_module(request, preview_id, descriptor):
+ """
+ Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
+ from the set of preview data for the descriptor specified by Location
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ location: A Location
+ """
+
+ return load_preview_module(request, preview_id, descriptor)
+
+
+def load_preview_module(request, preview_id, descriptor):
+ """
+ Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
+
+ request: The active django request
+ preview_id (str): An identifier specifying which preview this module is used for
+ descriptor: An XModuleDescriptor
+ instance_state: An instance state string
+ shared_state: A shared state string
+ """
+ system = preview_module_system(request, preview_id, descriptor)
+ try:
+ module = descriptor.xmodule(system)
+ except:
+ log.debug("Unable to load preview module", exc_info=True)
+ module = ErrorDescriptor.from_descriptor(
+ descriptor,
+ error_msg=exc_info_to_str(sys.exc_info())
+ ).xmodule(system)
+
+ # cdodge: Special case
+ if module.location.category == 'static_tab':
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_tab_display.html",
+ )
+ else:
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_display.html",
+ )
+
+ module.get_html = replace_static_urls(
+ module.get_html,
+ getattr(module, 'data_dir', module.location.course),
+ course_namespace=Location([module.location.tag, module.location.org, module.location.course, None, None])
+ )
+
+ return module
+
+def get_module_previews(request, descriptor):
+ """
+ Returns a list of preview XModule html contents. One preview is returned for each
+ pair of states returned by get_sample_state() for the supplied descriptor.
+
+ descriptor: An XModuleDescriptor
+ """
+ preview_html = []
+ for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
+ module = load_preview_module(request, str(idx), descriptor)
+ preview_html.append(module.get_html())
+ return preview_html
+
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
new file mode 100644
index 0000000000..7c207b7893
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -0,0 +1,50 @@
+from external_auth.views import ssl_login_shortcut
+from mitxmako.shortcuts import render_to_response
+from django_future.csrf import ensure_csrf_cookie
+from requests import index
+
+"""
+Public views
+"""
+
+@ensure_csrf_cookie
+def signup(request):
+ """
+ Display the signup form.
+ """
+ csrf_token = csrf(request)['csrf_token']
+ return render_to_response('signup.html', {'csrf': csrf_token})
+
+
+def old_login_redirect(request):
+ '''
+ Redirect to the active login url.
+ '''
+ return redirect('login', permanent=True)
+
+
+@ssl_login_shortcut
+@ensure_csrf_cookie
+def login_page(request):
+ """
+ Display the login form.
+ """
+ csrf_token = csrf(request)['csrf_token']
+ return render_to_response('login.html', {
+ 'csrf': csrf_token,
+ 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
+ })
+
+
+def howitworks(request):
+ if request.user.is_authenticated():
+ return index(request)
+ else:
+ return render_to_response('howitworks.html', {})
+
+def ux_alerts(request):
+ """
+ static/proof-of-concept views
+ """
+ return render_to_response('ux-alerts.html', {})
+
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
new file mode 100644
index 0000000000..131068768a
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -0,0 +1,91 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from access import has_access
+from contentstore.utils import get_url_reverse, get_lms_link_for_item
+from django.conf import settings
+
+@login_required
+@ensure_csrf_cookie
+def index(request):
+ """
+ List all courses available to the logged in user
+ """
+ courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
+
+ # filter out courses that we don't have access too
+ def course_filter(course):
+ return (has_access(request.user, course.location)
+ and course.location.course != 'templates'
+ and course.location.org != ''
+ and course.location.course != ''
+ and course.location.name != '')
+ courses = filter(course_filter, courses)
+
+ return render_to_response('index.html', {
+ 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
+ 'courses': [(course.display_name,
+ get_url_reverse('CourseOutline', course),
+ get_lms_link_for_item(course.location, course_id=course.location.course_id))
+ for course in courses],
+ 'user': request.user,
+ 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
+ })
+
+
+# ==== Views with per-item permissions================================
+
+
+# points to the temporary course landing page with log in and sign up
+def landing(request, org, course, coursename):
+ return render_to_response('temp-course-landing.html', {})
+
+# points to the temporary edge page
+def edge(request):
+ return render_to_response('university_profiles/edge.html', {})
+
+
+def event(request):
+ '''
+ A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
+ console logs don't get distracted :-)
+ '''
+ return HttpResponse(True)
+
+
+def get_request_method(request):
+ """
+ Using HTTP_X_HTTP_METHOD_OVERRIDE, in the request metadata, determine
+ what type of request came from the client, and return it.
+ """
+ # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
+ if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
+ real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
+ else:
+ real_method = request.method
+
+ return real_method
+
+def create_json_response(errmsg=None):
+ if errmsg is not None:
+ resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
+ else:
+ resp = HttpResponse(json.dumps({'Status': 'OK'}))
+
+ return resp
+
+def render_from_lms(template_name, dictionary, context=None, namespace='main'):
+ """
+ Render a template using the LMS MAKO_TEMPLATES
+ """
+ return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
+
+
+def _xmodule_recurse(item, action):
+ for child in item.get_children():
+ _xmodule_recurse(child, action)
+
+ action(item)
+
diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py
new file mode 100644
index 0000000000..2f6868ee81
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/session_kv_store.py
@@ -0,0 +1,27 @@
+from xblock.runtime import KeyValueStore
+
+class SessionKeyValueStore(KeyValueStore):
+ def __init__(self, request, model_data):
+ self._model_data = model_data
+ self._session = request.session
+
+ def get(self, key):
+ try:
+ return self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ return self._session[tuple(key)]
+
+ def set(self, key, value):
+ try:
+ self._model_data[key.field_name] = value
+ except (KeyError, InvalidScopeError):
+ self._session[tuple(key)] = value
+
+ def delete(self, key):
+ try:
+ del self._model_data[key.field_name]
+ except (KeyError, InvalidScopeError):
+ del self._session[tuple(key)]
+
+ def has(self, key):
+ return key in self._model_data or key in self._session
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
new file mode 100644
index 0000000000..5be78a0c37
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -0,0 +1,107 @@
+from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+
+from util.json_request import expect_json
+from mitxmako.shortcuts import render_to_response
+
+def user_author_string(user):
+ '''Get an author string for commits by this user. Format:
+ first last .
+
+ If the first and last names are blank, uses the username instead.
+ Assumes that the email is not blank.
+ '''
+ f = user.first_name
+ l = user.last_name
+ if f == '' and l == '':
+ f = user.username
+ return '{first} {last} <{email}>'.format(first=f,
+ last=l,
+ email=user.email)
+
+
+@login_required
+@ensure_csrf_cookie
+def manage_users(request, location):
+ '''
+ This view will return all CMS users who are editors for the specified course
+ '''
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
+ raise PermissionDenied()
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('manage_users.html', {
+ 'active_tab': 'users',
+ 'context_course': course_module,
+ 'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
+ 'add_user_postback_url': reverse('add_user', args=[location]).rstrip('/'),
+ 'remove_user_postback_url': reverse('remove_user', args=[location]).rstrip('/'),
+ 'allow_actions': has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
+ 'request_user_id': request.user.id
+ })
+
+
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def add_user(request, location):
+ '''
+ This POST-back view will add a user - specified by email - to the list of editors for
+ the specified course
+ '''
+ email = request.POST["email"]
+
+ if email == '':
+ return create_json_response('Please specify an email address.')
+
+ # check that logged in user has admin permissions to this course
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
+ raise PermissionDenied()
+
+ user = get_user_by_email(email)
+
+ # user doesn't exist?!? Return error.
+ if user is None:
+ return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
+
+ # user exists, but hasn't activated account?!?
+ if not user.is_active:
+ return create_json_response('User {0} has registered but has not yet activated his/her account.'.format(email))
+
+ # ok, we're cool to add to the course group
+ add_user_to_course_group(request.user, user, location, STAFF_ROLE_NAME)
+
+ return create_json_response()
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def remove_user(request, location):
+ '''
+ This POST-back view will remove a user - specified by email - from the list of editors for
+ the specified course
+ '''
+
+ email = request.POST["email"]
+
+ # check that logged in user has admin permissions on this course
+ if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
+ raise PermissionDenied()
+
+ user = get_user_by_email(email)
+ if user is None:
+ return create_json_response('Could not find user by email address \'{0}\'.'.format(email))
+
+ # make sure we're not removing ourselves
+ if user.id == request.user.id:
+ raise PermissionDenied()
+
+ remove_user_from_course_group(request.user, user, location, STAFF_ROLE_NAME)
+
+ return create_json_response()
+
From 89d0dc196304d5afd9a42e5ec70b9ed10bb9ade9 Mon Sep 17 00:00:00 2001
From: Greg Price
Date: Tue, 7 May 2013 15:54:34 -0400
Subject: [PATCH 039/123] Update "thank you" text in help modal
As per request from Mary.
---
lms/templates/help_modal.html | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/lms/templates/help_modal.html b/lms/templates/help_modal.html
index 83ea00068f..deebd391d2 100644
--- a/lms/templates/help_modal.html
+++ b/lms/templates/help_modal.html
@@ -1,4 +1,6 @@
<%namespace name='static' file='static_content.html'/>
+<%! from datetime import datetime %>
+<%! import pytz %>
<%! from django.conf import settings %>
<%! from courseware.tabs import get_discussion_link %>
@@ -79,9 +81,16 @@ discussion_link = get_discussion_link(course) if course else None
+ <%
+ dst = datetime.now(pytz.utc).astimezone(pytz.timezone("America/New_York")).dst()
+ business_hours = "13:00 UTC to 21:00 UTC" if dst else "14:00 UTC to 22:00 UTC"
+ %>
- Thanks for your feedback. We will read your message, and our
- support team may contact you to respond or ask for further clarification.
+ Thank you for your inquiry or feedback. We typically respond to a
+ request within one business day (Monday to Friday,
+ ${business_hours}.) In the meantime, please review our
+ detailed FAQs
+ where most questions have already been answered.
Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.
+
The Studio servers encountered an error
+
+ An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
+ We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ If the problem persists, please email us at technical@edx.org.
+
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
- We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at technical@edx.org.
From 9350a2c0674eee804ba7aae96c6d358a5156b291 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 13:56:30 -0400
Subject: [PATCH 044/123] refactoring views
---
cms/djangoapps/contentstore/views/__init__.py | 28 +-
cms/djangoapps/contentstore/views/access.py | 6 +-
cms/djangoapps/contentstore/views/assets.py | 142 +++++-
.../contentstore/views/checklist.py | 9 +-
.../contentstore/views/component.py | 313 +++++++++++++
cms/djangoapps/contentstore/views/course.py | 420 ++++++++++++------
cms/djangoapps/contentstore/views/error.py | 2 +-
cms/djangoapps/contentstore/views/item.py | 16 +-
cms/djangoapps/contentstore/views/preview.py | 15 +
cms/djangoapps/contentstore/views/public.py | 6 +-
cms/djangoapps/contentstore/views/requests.py | 41 +-
.../contentstore/views/session_kv_store.py | 2 +-
cms/djangoapps/contentstore/views/tabs.py | 110 +++++
cms/djangoapps/contentstore/views/user.py | 43 +-
14 files changed, 956 insertions(+), 197 deletions(-)
create mode 100644 cms/djangoapps/contentstore/views/component.py
create mode 100644 cms/djangoapps/contentstore/views/tabs.py
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index 0b7c271b1e..37f786ac3c 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,17 +1,13 @@
-from new import *
-from error import *
+# TODO: replace asterisks, should explicitly enumerate imports instead
+
+from assets import asset_index, upload_asset, import_course, generate_export_course, export_course
+from checklist import get_checklists, update_checklist
+from component import *
from course import *
-from item import *
-from public import *
-from user import *
-from preview import *
-from assets import *
-from checklist import *
-from requests import landing
-
-
-"""
-
-from main import *
-
-"""
+from error import not_found, server_error, render_404, render_500
+from item import save_item, clone_item, delete_item
+from preview import preview_dispatch, preview_component
+from public import signup, old_login_redirect, login_page, howitworks, ux_alerts
+from user import index, add_user, remove_user, manage_users
+from tabs import edit_tabs, reorder_static_tabs
+from requests import edge, event, landing
diff --git a/cms/djangoapps/contentstore/views/access.py b/cms/djangoapps/contentstore/views/access.py
index dd3add1099..37f6fcb767 100644
--- a/cms/djangoapps/contentstore/views/access.py
+++ b/cms/djangoapps/contentstore/views/access.py
@@ -1,11 +1,7 @@
-#from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
-#from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
-#from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
-
from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME
from auth.authz import is_user_in_course_group_role
from contentstore.utils import get_course_location_for_item
-
+from django.core.exceptions import PermissionDenied
def get_location_and_verify_access(request, org, course, name):
"""
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index 616b04342d..c2aa52b1e1 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -1,12 +1,29 @@
+import logging, json, os, tarfile, shutil
+from tempfile import mkdtemp
+from path import path
+
+from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
+from django.core.urlresolvers import reverse
+from django.core.servers.basehttp import FileWrapper
+from django.core.files.temp import NamedTemporaryFile
-from xmodule.contentstore.content import StaticContent
-from access import get_location_and_verify_access
-from xmodule.util.date_utils import get_default_time_display
from mitxmako.shortcuts import render_to_response
+from cache_toolbox.core import del_cached_content
+from contentstore.utils import get_url_reverse
+from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.contentstore.django import contentstore
+from xmodule.modulestore.xml_exporter import export_to_xml
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore import Location
+from xmodule.contentstore.content import StaticContent
+from xmodule.util.date_utils import get_default_time_display
+
+from access import get_location_and_verify_access
+from auth.authz import create_all_course_groups
@login_required
@ensure_csrf_cookie
@@ -116,3 +133,122 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response
+@ensure_csrf_cookie
+@login_required
+def import_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ if request.method == 'POST':
+ filename = request.FILES['course-data'].name
+
+ if not filename.endswith('.tar.gz'):
+ return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
+
+ data_root = path(settings.GITHUB_REPO_ROOT)
+
+ course_subdir = "{0}-{1}-{2}".format(org, course, name)
+ course_dir = data_root / course_subdir
+ if not course_dir.isdir():
+ os.mkdir(course_dir)
+
+ temp_filepath = course_dir / filename
+
+ logging.debug('importing course to {0}'.format(temp_filepath))
+
+ # stream out the uploaded files in chunks to disk
+ temp_file = open(temp_filepath, 'wb+')
+ for chunk in request.FILES['course-data'].chunks():
+ temp_file.write(chunk)
+ temp_file.close()
+
+ tf = tarfile.open(temp_filepath)
+ tf.extractall(course_dir + '/')
+
+ # 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
+
+ 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)
+
+ module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
+ [course_subdir], load_error_modules=False,
+ static_content_store=contentstore(),
+ target_location_namespace=Location(location),
+ draft_store=modulestore())
+
+ # 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))
+
+ create_all_course_groups(request.user, course_items[0].location)
+
+ return HttpResponse(json.dumps({'Status': 'OK'}))
+ else:
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('import.html', {
+ 'context_course': course_module,
+ 'active_tab': 'import',
+ 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
+ })
+
+
+@ensure_csrf_cookie
+@login_required
+def generate_export_course(request, org, course, name):
+ location = get_location_and_verify_access(request, org, course, name)
+
+ loc = Location(location)
+ export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
+
+ root_dir = path(mkdtemp())
+
+ # export out to a tempdir
+
+ logging.debug('root = {0}'.format(root_dir))
+
+ export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
+ #filename = root_dir / name + '.tar.gz'
+
+ logging.debug('tar file being generated at {0}'.format(export_file.name))
+ tf = tarfile.open(name=export_file.name, mode='w:gz')
+ tf.add(root_dir / name, arcname=name)
+ tf.close()
+
+ # remove temp dir
+ shutil.rmtree(root_dir / name)
+
+ wrapper = FileWrapper(export_file)
+ response = HttpResponse(wrapper, content_type='application/x-tgz')
+ response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
+ response['Content-Length'] = os.path.getsize(export_file.name)
+ return response
+
+@ensure_csrf_cookie
+@login_required
+def export_course(request, org, course, name):
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('export.html', {
+ 'context_course': course_module,
+ 'active_tab': 'export',
+ 'successful_import_redirect_url': ''
+ })
+
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index 376e041523..4a97ddc1df 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -1,9 +1,16 @@
+import json
+
from django.http import HttpResponse, HttpResponseBadRequest
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-from access import get_location_and_verify_access
from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.inheritance import own_metadata
+
from contentstore.utils import get_modulestore, get_url_reverse
+from requests import get_request_method
+from access import get_location_and_verify_access
@ensure_csrf_cookie
@login_required
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
new file mode 100644
index 0000000000..2dd7307976
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -0,0 +1,313 @@
+import json, logging
+from collections import defaultdict
+
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django_future.csrf import ensure_csrf_cookie
+from django.conf import settings
+
+from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from xmodule.util.date_utils import get_default_time_display
+
+from xblock.core import Scope
+from util.json_request import expect_json
+
+from contentstore.module_info_model import get_module_info, set_module_info
+from contentstore.utils import get_modulestore, get_lms_link_for_item, \
+ compute_unit_state, UnitState, get_course_for_item
+
+from models.settings.course_grading import CourseGradingModel
+
+from requests import get_request_method, _xmodule_recurse
+from access import has_access, get_location_and_verify_access
+
+# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
+
+log = logging.getLogger(__name__)
+
+COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
+
+OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
+ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
+ADVANCED_COMPONENT_CATEGORY = 'advanced'
+ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
+
+
+@login_required
+def edit_subsection(request, location):
+ # check that we have permissions to edit this item
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location, depth=1)
+
+ lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
+ preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
+
+ # make sure that location references a 'sequential', otherwise return BadRequest
+ if item.location.category != 'sequential':
+ return HttpResponseBadRequest()
+
+ parent_locs = modulestore().get_parent_locations(location, None)
+
+ # we're for now assuming a single parent
+ if len(parent_locs) != 1:
+ logging.error('Multiple (or none) parents have been found for {0}'.format(location))
+
+ # this should blow up if we don't find any parents, which would be erroneous
+ parent = modulestore().get_item(parent_locs[0])
+
+ # remove all metadata from the generic dictionary that is presented in a more normalized UI
+
+ policy_metadata = dict(
+ (field.name, field.read_from(item))
+ for field
+ in item.fields
+ if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
+ )
+
+ can_view_live = False
+ subsection_units = item.get_children()
+ for unit in subsection_units:
+ state = compute_unit_state(unit)
+ if state == UnitState.public or state == UnitState.draft:
+ can_view_live = True
+ break
+
+ return render_to_response('edit_subsection.html',
+ {'subsection': item,
+ 'context_course': course,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'lms_link': lms_link,
+ 'preview_link': preview_link,
+ 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
+ 'parent_location': course.location,
+ 'parent_item': parent,
+ 'policy_metadata': policy_metadata,
+ 'subsection_units': subsection_units,
+ 'can_view_live': can_view_live
+ })
+
+
+@login_required
+def edit_unit(request, location):
+ """
+ Display an editing page for the specified module.
+
+ Expects a GET request with the parameter 'id'.
+
+ id: A Location URL
+ """
+ course = get_course_for_item(location)
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location, depth=1)
+
+ lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
+
+ component_templates = defaultdict(list)
+
+ # Check if there are any advanced modules specified in the course policy. These modules
+ # should be specified as a list of strings, where the strings are the names of the modules
+ # in ADVANCED_COMPONENT_TYPES that should be enabled for the course.
+ course_advanced_keys = course.advanced_modules
+
+ # Set component types according to course policy file
+ component_types = list(COMPONENT_TYPES)
+ if isinstance(course_advanced_keys, list):
+ course_advanced_keys = [c for c in course_advanced_keys if c in ADVANCED_COMPONENT_TYPES]
+ if len(course_advanced_keys) > 0:
+ component_types.append(ADVANCED_COMPONENT_CATEGORY)
+ else:
+ log.error("Improper format for course advanced keys! {0}".format(course_advanced_keys))
+
+ templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
+ for template in templates:
+ category = template.location.category
+
+ if category in course_advanced_keys:
+ category = ADVANCED_COMPONENT_CATEGORY
+
+ if category in component_types:
+ # This is a hack to create categories for different xmodules
+ component_templates[category].append((
+ template.display_name_with_default,
+ template.location.url(),
+ hasattr(template, 'markdown') and template.markdown is not None,
+ template.cms.empty,
+ ))
+
+ components = [
+ component.location.url()
+ for component
+ in item.get_children()
+ ]
+
+ # TODO (cpennington): If we share units between courses,
+ # this will need to change to check permissions correctly so as
+ # to pick the correct parent subsection
+
+ containing_subsection_locs = modulestore().get_parent_locations(location, None)
+ containing_subsection = modulestore().get_item(containing_subsection_locs[0])
+
+ containing_section_locs = modulestore().get_parent_locations(containing_subsection.location, None)
+ containing_section = modulestore().get_item(containing_section_locs[0])
+
+ # cdodge hack. We're having trouble previewing drafts via jump_to redirect
+ # so let's generate the link url here
+
+ # need to figure out where this item is in the list of children as the preview will need this
+ index = 1
+ for child in containing_subsection.get_children():
+ if child.location == item.location:
+ break
+ index = index + 1
+
+ preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
+ 'preview.' + settings.LMS_BASE)
+
+ preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
+ preview_lms_base=preview_lms_base,
+ lms_base=settings.LMS_BASE,
+ org=course.location.org,
+ course=course.location.course,
+ course_name=course.location.name,
+ section=containing_section.location.name,
+ subsection=containing_subsection.location.name,
+ index=index)
+
+ unit_state = compute_unit_state(item)
+
+ return render_to_response('unit.html', {
+ 'context_course': course,
+ 'active_tab': 'courseware',
+ 'unit': item,
+ 'unit_location': location,
+ 'components': components,
+ 'component_templates': component_templates,
+ 'draft_preview_link': preview_lms_link,
+ 'published_preview_link': lms_link,
+ 'subsection': containing_subsection,
+ 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
+ 'section': containing_section,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
+ 'unit_state': unit_state,
+ 'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
+ })
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def assignment_type_update(request, org, course, category, name):
+ '''
+ CRUD operations on assignment types for sections and subsections and anything else gradable.
+ '''
+ location = Location(['i4x', org, course, category, name])
+ if not has_access(request.user, location):
+ raise HttpResponseForbidden()
+
+ if request.method == 'GET':
+ return HttpResponse(json.dumps(CourseGradingModel.get_section_grader_type(location)),
+ mimetype="application/json")
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(CourseGradingModel.update_section_grader_type(location, request.POST)),
+ mimetype="application/json")
+
+
+@login_required
+@expect_json
+def create_draft(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ # This clones the existing item location to a draft location (the draft is implicit,
+ # because modulestore is a Draft modulestore)
+ modulestore().clone_item(location, location)
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def publish_draft(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location)
+ _xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
+
+ return HttpResponse()
+
+
+@login_required
+@expect_json
+def unpublish_unit(request):
+ location = request.POST['id']
+
+ # check permissions for this user within this course
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ item = modulestore().get_item(location)
+ _xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
+
+ return HttpResponse()
+
+
+@login_required
+@ensure_csrf_cookie
+def static_pages(request, org, course, coursename):
+
+ location = get_location_and_verify_access(request, org, course, coursename)
+
+ course = modulestore().get_item(location)
+
+ return render_to_response('static-pages.html', {
+ 'active_tab': 'pages',
+ 'context_course': course,
+ })
+
+
+def edit_static(request, org, course, coursename):
+ return render_to_response('edit-static-page.html', {})
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def module_info(request, module_location):
+ location = Location(module_location)
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ real_method = get_request_method(request)
+
+ rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
+ logging.debug('rewrite_static_links = {0} {1}'.format(request.GET.get('rewrite_url_links', 'False'), rewrite_static_links))
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ if real_method == 'GET':
+ return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location, rewrite_static_links=rewrite_static_links)), mimetype="application/json")
+ elif real_method == 'POST' or real_method == 'PUT':
+ return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest()
+
+
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index fc2214f970..c0f6acc808 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -1,9 +1,65 @@
+import json, time
+
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-
-from util.json_request import expect_json
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.modulestore import Location
+
+from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
+from contentstore.utils import get_lms_link_for_item, add_open_ended_panel_tab, remove_open_ended_panel_tab
+from models.settings.course_details import CourseDetails, CourseSettingsEncoder
+from models.settings.course_grading import CourseGradingModel
+from models.settings.course_metadata import CourseMetadata
+from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
+from auth.authz import create_all_course_groups
+from util.json_request import expect_json
+from access import has_access, get_location_and_verify_access
+from requests import get_request_method
+from tabs import initialize_course_tabs
+
+
+@login_required
+@ensure_csrf_cookie
+def course_index(request, org, course, name):
+ """
+ Display an editable course overview.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ lms_link = get_lms_link_for_item(location)
+
+ upload_asset_callback_url = reverse('upload_asset', kwargs={
+ 'org': org,
+ 'course': course,
+ 'coursename': name
+ })
+
+ course = modulestore().get_item(location, depth=3)
+ sections = course.get_children()
+
+ return render_to_response('overview.html', {
+ 'active_tab': 'courseware',
+ 'context_course': course,
+ 'lms_link': lms_link,
+ 'sections': sections,
+ 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
+ 'parent_location': course.location,
+ 'new_section_template': Location('i4x', 'edx', 'templates', 'chapter', 'Empty'),
+ 'new_subsection_template': Location('i4x', 'edx', 'templates', 'sequential', 'Empty'), # for now they are the same, but the could be different at some point...
+ 'upload_asset_callback_url': upload_asset_callback_url,
+ 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty')
+ })
+
+
@login_required
@expect_json
def create_new_course(request):
@@ -63,140 +119,246 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()}))
-def initialize_course_tabs(course):
- # set up the default tabs
- # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
- # at least a list populated with the minimal times
- # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
- # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
-
- # This logic is repeated in xmodule/modulestore/tests/factories.py
- # so if you change anything here, you need to also change it there.
- course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
-
-
-@ensure_csrf_cookie
@login_required
-def import_course(request, org, course, name):
-
- location = get_location_and_verify_access(request, org, course, name)
-
- if request.method == 'POST':
- filename = request.FILES['course-data'].name
-
- if not filename.endswith('.tar.gz'):
- return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
-
- data_root = path(settings.GITHUB_REPO_ROOT)
-
- course_subdir = "{0}-{1}-{2}".format(org, course, name)
- course_dir = data_root / course_subdir
- if not course_dir.isdir():
- os.mkdir(course_dir)
-
- temp_filepath = course_dir / filename
-
- logging.debug('importing course to {0}'.format(temp_filepath))
-
- # stream out the uploaded files in chunks to disk
- temp_file = open(temp_filepath, 'wb+')
- for chunk in request.FILES['course-data'].chunks():
- temp_file.write(chunk)
- temp_file.close()
-
- tf = tarfile.open(temp_filepath)
- tf.extractall(course_dir + '/')
-
- # 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
-
- 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)
-
- module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_subdir], load_error_modules=False,
- static_content_store=contentstore(),
- target_location_namespace=Location(location),
- draft_store=modulestore())
-
- # 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))
-
- create_all_course_groups(request.user, course_items[0].location)
-
- return HttpResponse(json.dumps({'Status': 'OK'}))
- else:
- course_module = modulestore().get_item(location)
-
- return render_to_response('import.html', {
- 'context_course': course_module,
- 'active_tab': 'import',
- 'successful_import_redirect_url': get_url_reverse('CourseOutline', course_module)
- })
-
-
@ensure_csrf_cookie
-@login_required
-def generate_export_course(request, org, course, name):
- location = get_location_and_verify_access(request, org, course, name)
-
- loc = Location(location)
- export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
-
- root_dir = path(mkdtemp())
-
- # export out to a tempdir
-
- logging.debug('root = {0}'.format(root_dir))
-
- export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
- #filename = root_dir / name + '.tar.gz'
-
- logging.debug('tar file being generated at {0}'.format(export_file.name))
- tf = tarfile.open(name=export_file.name, mode='w:gz')
- tf.add(root_dir / name, arcname=name)
- tf.close()
-
- # remove temp dir
- shutil.rmtree(root_dir / name)
-
- wrapper = FileWrapper(export_file)
- response = HttpResponse(wrapper, content_type='application/x-tgz')
- response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
- response['Content-Length'] = os.path.getsize(export_file.name)
- return response
-
-@ensure_csrf_cookie
-@login_required
-def export_course(request, org, course, name):
+def course_info(request, org, course, name, provided_id=None):
+ """
+ Send models and views as well as html for editing the course info to the client.
+ org, course, name: Attributes of the Location for the item to edit
+ """
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
- return render_to_response('export.html', {
+ # get current updates
+ location = ['i4x', org, course, 'course_info', "updates"]
+
+ return render_to_response('course_info.html', {
+ 'active_tab': 'courseinfo-tab',
'context_course': course_module,
- 'active_tab': 'export',
- 'successful_import_redirect_url': ''
+ 'url_base': "/" + org + "/" + course + "/",
+ 'course_updates': json.dumps(get_course_updates(location)),
+ 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_info_updates(request, org, course, provided_id=None):
+ """
+ restful CRUD operations on course_info updates.
+
+ org, course: Attributes of the Location for the item to edit
+ provided_id should be none if it's new (create) and a composite of the update db id + index otherwise.
+ """
+ # ??? No way to check for access permission afaik
+ # get current updates
+ location = ['i4x', org, course, 'course_info', "updates"]
+
+ # Hmmm, provided_id is coming as empty string on create whereas I believe it used to be None :-(
+ # Possibly due to my removing the seemingly redundant pattern in urls.py
+ if provided_id == '':
+ provided_id = None
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ real_method = get_request_method(request)
+
+ if request.method == 'GET':
+ return HttpResponse(json.dumps(get_course_updates(location)),
+ mimetype="application/json")
+ elif real_method == 'DELETE':
+ try:
+ return HttpResponse(json.dumps(delete_course_update(location,
+ request.POST, provided_id)), mimetype="application/json")
+ except:
+ return HttpResponseBadRequest("Failed to delete",
+ content_type="text/plain")
+ elif request.method == 'POST':
+ try:
+ return HttpResponse(json.dumps(update_course_updates(location,
+ request.POST, provided_id)), mimetype="application/json")
+ except:
+ return HttpResponseBadRequest("Failed to save",
+ content_type="text/plain")
+
+@login_required
+@ensure_csrf_cookie
+def get_course_settings(request, org, course, name):
+ """
+ Send models and views as well as html for editing the course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('settings.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'details_url': reverse(course_settings_updates,
+ kwargs={"org": org,
+ "course": course,
+ "name": name,
+ "section": "details"})
+ })
+
+
+@login_required
+@ensure_csrf_cookie
+def course_config_graders_page(request, org, course, name):
+ """
+ Send models and views as well as html for editing the course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+ course_details = CourseGradingModel.fetch(location)
+
+ return render_to_response('settings_graders.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
+ })
+
+
+@login_required
+@ensure_csrf_cookie
+def course_config_advanced_page(request, org, course, name):
+ """
+ Send models and views as well as html for editing the advanced course settings to the client.
+
+ org, course, name: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ course_module = modulestore().get_item(location)
+
+ return render_to_response('settings_advanced.html', {
+ 'context_course': course_module,
+ 'course_location': location,
+ 'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
+ })
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_settings_updates(request, org, course, name, section):
+ """
+ restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
+ through json (not rendering any html) and handles section level operations rather than whole page.
+
+ org, course: Attributes of the Location for the item to edit
+ section: one of details, faculty, grading, problems, discussions
+ """
+ get_location_and_verify_access(request, org, course, name)
+
+ if section == 'details':
+ manager = CourseDetails
+ elif section == 'grading':
+ manager = CourseGradingModel
+ else:
+ return
+
+ if request.method == 'GET':
+ # Cannot just do a get w/o knowing the course name :-(
+ return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course', name])), cls=CourseSettingsEncoder),
+ mimetype="application/json")
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
+ mimetype="application/json")
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def course_grader_updates(request, org, course, name, grader_index=None):
+ """
+ restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
+ through json (not rendering any html) and handles section level operations rather than whole page.
+
+ org, course: Attributes of the Location for the item to edit
+ """
+
+ location = get_location_and_verify_access(request, org, course, name)
+
+ real_method = get_request_method(request)
+
+ if real_method == 'GET':
+ # Cannot just do a get w/o knowing the course name :-(
+ return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(location), grader_index)),
+ mimetype="application/json")
+ elif real_method == "DELETE":
+ # ??? Should this return anything? Perhaps success fail?
+ CourseGradingModel.delete_grader(Location(location), grader_index)
+ return HttpResponse()
+ elif request.method == 'POST': # post or put, doesn't matter.
+ return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(location), request.POST)),
+ mimetype="application/json")
+
+
+# # NB: expect_json failed on ["key", "key2"] and json payload
+@login_required
+@ensure_csrf_cookie
+def course_advanced_updates(request, org, course, name):
+ """
+ restful CRUD operations on metadata. The payload is a json rep of the metadata dicts. For delete, otoh,
+ the payload is either a key or a list of keys to delete.
+
+ org, course: Attributes of the Location for the item to edit
+ """
+ location = get_location_and_verify_access(request, org, course, name)
+
+ real_method = get_request_method(request)
+
+ if real_method == 'GET':
+ return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
+ elif real_method == 'DELETE':
+ return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
+ mimetype="application/json")
+ elif real_method == 'POST' or real_method == 'PUT':
+ # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
+ request_body = json.loads(request.body)
+ #Whether or not to filter the tabs key out of the settings metadata
+ filter_tabs = True
+ #Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
+ #to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
+ #module, and to remove it if they have removed the open ended elements.
+ if ADVANCED_COMPONENT_POLICY_KEY in request_body:
+ #Check to see if the user instantiated any open ended components
+ found_oe_type = False
+ #Get the course so that we can scrape current tabs
+ course_module = modulestore().get_item(location)
+ for oe_type in OPEN_ENDED_COMPONENT_TYPES:
+ if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
+ #Add an open ended tab to the course if needed
+ changed, new_tabs = add_open_ended_panel_tab(course_module)
+ #If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ #Set this flag to avoid the open ended tab removal code below.
+ found_oe_type = True
+ break
+ #If we did not find an open ended module type in the advanced settings,
+ # we may need to remove the open ended tab from the course.
+ if not found_oe_type:
+ #Remove open ended tab to the course if needed
+ changed, new_tabs = remove_open_ended_panel_tab(course_module)
+ if changed:
+ request_body.update({'tabs': new_tabs})
+ #Indicate that tabs should not be filtered out of the metadata
+ filter_tabs = False
+ response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
+ return HttpResponse(response_json, mimetype="application/json")
+
+
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
index 527f137b9e..814af96104 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -1,4 +1,4 @@
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 876251203e..b6d03e3f81 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -1,7 +1,21 @@
+import json
+from uuid import uuid4
+
+from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.inheritance import own_metadata
+
from util.json_request import expect_json
-from mitxmako.shortcuts import render_to_response
+from contentstore.utils import get_modulestore
+from access import has_access
+from requests import _xmodule_recurse
+
+# cdodge: these are categories which should not be parented, they are detached from the hierarchy
+DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index f473b962c5..a3fc816730 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,3 +1,10 @@
+import logging, sys
+import static_replace
+from xmodule_modifiers import replace_static_urls
+from xmodule.error_module import ErrorDescriptor
+from xmodule.errortracker import exc_info_to_str
+from django.core.urlresolvers import reverse
+from mitxmako.shortcuts import render_to_response
from django.contrib.auth.decorators import login_required
from xblock.runtime import DbModel
from xmodule.x_module import ModuleSystem
@@ -6,6 +13,14 @@ from xmodule_modifiers import wrap_xmodule
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from functools import partial
+from xmodule.modulestore import Location
+from access import has_access
+from xmodule.modulestore.django import modulestore
+from xmodule.exceptions import NotFoundError, ProcessingError
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+
+
+log = logging.getLogger(__name__)
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index 7c207b7893..fe26fbec7c 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -1,7 +1,11 @@
from external_auth.views import ssl_login_shortcut
from mitxmako.shortcuts import render_to_response
from django_future.csrf import ensure_csrf_cookie
-from requests import index
+from django.core.context_processors import csrf
+from django.shortcuts import redirect
+from django.conf import settings
+
+from user import index
"""
Public views
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 131068768a..58a3275527 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -1,42 +1,7 @@
-from django.contrib.auth.decorators import login_required
-from django_future.csrf import ensure_csrf_cookie
-from mitxmako.shortcuts import render_to_response
-from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from access import has_access
-from contentstore.utils import get_url_reverse, get_lms_link_for_item
-from django.conf import settings
-
-@login_required
-@ensure_csrf_cookie
-def index(request):
- """
- List all courses available to the logged in user
- """
- courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
-
- # filter out courses that we don't have access too
- def course_filter(course):
- return (has_access(request.user, course.location)
- and course.location.course != 'templates'
- and course.location.org != ''
- and course.location.course != ''
- and course.location.name != '')
- courses = filter(course_filter, courses)
-
- return render_to_response('index.html', {
- 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
- 'courses': [(course.display_name,
- get_url_reverse('CourseOutline', course),
- get_lms_link_for_item(course.location, course_id=course.location.course_id))
- for course in courses],
- 'user': request.user,
- 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
- })
-
-
-# ==== Views with per-item permissions================================
+import json
+from django.http import HttpResponse
+from mitxmako.shortcuts import render_to_string, render_to_response
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
diff --git a/cms/djangoapps/contentstore/views/session_kv_store.py b/cms/djangoapps/contentstore/views/session_kv_store.py
index 2f6868ee81..7bfb14351d 100644
--- a/cms/djangoapps/contentstore/views/session_kv_store.py
+++ b/cms/djangoapps/contentstore/views/session_kv_store.py
@@ -1,4 +1,4 @@
-from xblock.runtime import KeyValueStore
+from xblock.runtime import KeyValueStore, InvalidScopeError
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
new file mode 100644
index 0000000000..9a6d8736bf
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -0,0 +1,110 @@
+from access import has_access
+from util.json_request import expect_json
+
+from django.http import HttpResponse, HttpResponseBadRequest
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django_future.csrf import ensure_csrf_cookie
+from mitxmako.shortcuts import render_to_response
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.inheritance import own_metadata
+from xmodule.modulestore.django import modulestore
+from contentstore.utils import get_course_for_item
+
+
+def initialize_course_tabs(course):
+ # set up the default tabs
+ # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
+ # at least a list populated with the minimal times
+ # @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
+ # place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
+
+
+@login_required
+@expect_json
+def reorder_static_tabs(request):
+ tabs = request.POST['tabs']
+ course = get_course_for_item(tabs[0])
+
+ if not has_access(request.user, course.location):
+ raise PermissionDenied()
+
+ # get list of existing static tabs in course
+ # make sure they are the same lengths (i.e. the number of passed in tabs equals the number
+ # that we know about) otherwise we can drop some!
+
+ existing_static_tabs = [t for t in course.tabs if t['type'] == 'static_tab']
+ if len(existing_static_tabs) != len(tabs):
+ return HttpResponseBadRequest()
+
+ # load all reference tabs, return BadRequest if we can't find any of them
+ tab_items = []
+ for tab in tabs:
+ item = modulestore('direct').get_item(Location(tab))
+ if item is None:
+ return HttpResponseBadRequest()
+
+ tab_items.append(item)
+
+ # now just go through the existing course_tabs and re-order the static tabs
+ reordered_tabs = []
+ static_tab_idx = 0
+ for tab in course.tabs:
+ if tab['type'] == 'static_tab':
+ reordered_tabs.append({'type': 'static_tab',
+ 'name': tab_items[static_tab_idx].display_name,
+ 'url_slug': tab_items[static_tab_idx].location.name})
+ static_tab_idx += 1
+ else:
+ reordered_tabs.append(tab)
+
+ # OK, re-assemble the static tabs in the new order
+ course.tabs = reordered_tabs
+ modulestore('direct').update_metadata(course.location, own_metadata(course))
+ return HttpResponse()
+
+@login_required
+@ensure_csrf_cookie
+def edit_tabs(request, org, course, coursename):
+ location = ['i4x', org, course, 'course', coursename]
+ course_item = modulestore().get_item(location)
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ # see tabs have been uninitialized (e.g. supporing courses created before tab support in studio)
+ if course_item.tabs is None or len(course_item.tabs) == 0:
+ initialize_course_tabs(course_item)
+
+ # first get all static tabs from the tabs list
+ # we do this because this is also the order in which items are displayed in the LMS
+ static_tabs_refs = [t for t in course_item.tabs if t['type'] == 'static_tab']
+
+ static_tabs = []
+ for static_tab_ref in static_tabs_refs:
+ static_tab_loc = Location(location)._replace(category='static_tab', name=static_tab_ref['url_slug'])
+ static_tabs.append(modulestore('direct').get_item(static_tab_loc))
+
+ components = [
+ static_tab.location.url()
+ for static_tab
+ in static_tabs
+ ]
+
+ return render_to_response('edit-tabs.html', {
+ 'active_tab': 'pages',
+ 'context_course': course_item,
+ 'components': components
+ })
diff --git a/cms/djangoapps/contentstore/views/user.py b/cms/djangoapps/contentstore/views/user.py
index 5be78a0c37..0ead03257b 100644
--- a/cms/djangoapps/contentstore/views/user.py
+++ b/cms/djangoapps/contentstore/views/user.py
@@ -1,9 +1,23 @@
+from django.conf import settings
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
-from util.json_request import expect_json
from mitxmako.shortcuts import render_to_response
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from contentstore.utils import get_url_reverse, get_lms_link_for_item
+
+from access import has_access
+from requests import create_json_response
+from util.json_request import expect_json
+
+from auth.authz import STAFF_ROLE_NAME, INSTRUCTOR_ROLE_NAME, get_users_in_course_group_by_role
+from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
+
+
def user_author_string(user):
'''Get an author string for commits by this user. Format:
first last .
@@ -20,6 +34,33 @@ def user_author_string(user):
email=user.email)
+@login_required
+@ensure_csrf_cookie
+def index(request):
+ """
+ List all courses available to the logged in user
+ """
+ courses = modulestore('direct').get_items(['i4x', None, None, 'course', None])
+
+ # filter out courses that we don't have access too
+ def course_filter(course):
+ return (has_access(request.user, course.location)
+ and course.location.course != 'templates'
+ and course.location.org != ''
+ and course.location.course != ''
+ and course.location.name != '')
+ courses = filter(course_filter, courses)
+
+ return render_to_response('index.html', {
+ 'new_course_template': Location('i4x', 'edx', 'templates', 'course', 'Empty'),
+ 'courses': [(course.display_name,
+ get_url_reverse('CourseOutline', course),
+ get_lms_link_for_item(course.location, course_id=course.location.course_id))
+ for course in courses],
+ 'user': request.user,
+ 'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
+ })
+
@login_required
@ensure_csrf_cookie
def manage_users(request, location):
From 01d4fbeb7c9915d7ad3c31dbdb36594d48d49ff3 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:14:36 -0400
Subject: [PATCH 045/123] reorder imports
---
cms/djangoapps/contentstore/views/preview.py | 27 ++++++++++----------
cms/djangoapps/contentstore/views/public.py | 5 ++--
2 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index a3fc816730..36ca01ec86 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,24 +1,25 @@
import logging, sys
import static_replace
-from xmodule_modifiers import replace_static_urls
+from functools import partial
+
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+from django.core.urlresolvers import reverse
+from django.contrib.auth.decorators import login_required
+from mitxmako.shortcuts import render_to_response
+
+from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
-from django.core.urlresolvers import reverse
-from mitxmako.shortcuts import render_to_response
-from django.contrib.auth.decorators import login_required
-from xblock.runtime import DbModel
-from xmodule.x_module import ModuleSystem
+from xmodule.exceptions import NotFoundError, ProcessingError
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
from xmodule.modulestore.mongo import MongoUsage
-from xmodule_modifiers import wrap_xmodule
+from xmodule.x_module import ModuleSystem
+from xblock.runtime import DbModel
+
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
-from functools import partial
-from xmodule.modulestore import Location
from access import has_access
-from xmodule.modulestore.django import modulestore
-from xmodule.exceptions import NotFoundError, ProcessingError
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
-
log = logging.getLogger(__name__)
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index fe26fbec7c..3ab9e4e5a0 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -1,10 +1,11 @@
-from external_auth.views import ssl_login_shortcut
-from mitxmako.shortcuts import render_to_response
from django_future.csrf import ensure_csrf_cookie
from django.core.context_processors import csrf
from django.shortcuts import redirect
from django.conf import settings
+from mitxmako.shortcuts import render_to_response
+
+from external_auth.views import ssl_login_shortcut
from user import index
"""
From 35d72f30b905dc2132d7d4bda428c48b97b782af Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:40:30 -0400
Subject: [PATCH 046/123] update .gitignore; fix logger import in execute.py
---
.gitignore | 3 +++
i18n/execute.py | 5 ++---
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
index d01baf055a..f1784a48f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.swp
*.orig
*.DS_Store
+*.mo
:2e_*
:2e#
.AppleDouble
@@ -22,6 +23,8 @@ reports/
*.egg-info
Gemfile.lock
.env/
+conf/locale/en/LC_MESSAGES/*.po
+!messages.po
lms/static/sass/*.css
cms/static/sass/*.css
lms/lib/comment_client/python
diff --git a/i18n/execute.py b/i18n/execute.py
index e3f3478d12..e55e653ea7 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -1,9 +1,8 @@
-import os, subprocess
+import os, subprocess, logging
-from logger import get_logger
from config import CONFIGURATION, BASE_DIR
-LOG = get_logger(__name__)
+LOG = logging.getLogger(__name__)
def execute(command, working_directory=BASE_DIR, log=LOG):
"""
From f30f6207d5e46938daf674d8685b1236c63be138 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:47:47 -0400
Subject: [PATCH 047/123] clean up rakefile syntax for task dependencies
---
rakefile | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/rakefile b/rakefile
index 3edbc39067..cf9363d47b 100644
--- a/rakefile
+++ b/rakefile
@@ -513,14 +513,12 @@ end
namespace :i18n do
desc "Extract localizable strings from sources"
- task :extract do
- Rake::Task["i18n:validate:gettext"].execute
+ task :extract => "i18n:validate:gettext" do
sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end
desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
- task :generate do
- Rake::Task["i18n:validate:gettext"].execute
+ task :generate => "i18n:validate:gettext" do
if ARGV.last.downcase == 'extract'
Rake::Task["i18n:extract"].execute
end
@@ -579,8 +577,7 @@ namespace :i18n do
end
desc "Run tests for the internationalization library"
- task :test do
- Rake::Task["i18n:validate:gettext"].execute
+ task :test => "i18n:validate:gettext" do
test = File.join(REPO_ROOT, "i18n", "tests")
sh("nosetests #{test}")
end
From a52cf85c0811d8bfd77b96676873550d0910da30 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 14:50:49 -0400
Subject: [PATCH 048/123] rake task dependency for transifex
---
rakefile | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/rakefile b/rakefile
index cf9363d47b..04a7db4904 100644
--- a/rakefile
+++ b/rakefile
@@ -562,15 +562,13 @@ namespace :i18n do
namespace :transifex do
desc "Push source strings to Transifex for translation"
- task :push do
- Rake::Task["i18n:validate:transifex_config"].execute
+ task :push => "i18n:validate:transifex_config" do
cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} push")
end
desc "Pull translated strings from Transifex"
- task :pull do
- Rake::Task["i18n:validate:transifex_config"].execute
+ task :pull => "i18n:validate:transifex_config" do
cmd = File.join(REPO_ROOT, "i18n", "transifex.py")
sh("#{cmd} pull")
end
From 1c5815a8444a1f8e8fa406fbced565468fb3d731 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 15:11:10 -0400
Subject: [PATCH 049/123] per-file log objects
---
i18n/execute.py | 14 +++++++++-----
i18n/tests/test_validate.py | 2 +-
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/i18n/execute.py b/i18n/execute.py
index e55e653ea7..1ff439ef38 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -4,25 +4,28 @@ from config import CONFIGURATION, BASE_DIR
LOG = logging.getLogger(__name__)
-def execute(command, working_directory=BASE_DIR, log=LOG):
+def execute(command, working_directory=BASE_DIR, log=True):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
- The command is logged to log, output is ignored.
+ log is boolean. If true, the command's invocation string is logged.
+ Output is ignored.
"""
if log:
- log.info(command)
+ LOG.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
-def call(command, working_directory=BASE_DIR, log=LOG):
+def call(command, working_directory=BASE_DIR, log=True):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
Returns a tuple of two strings: (stdout, stderr)
+ log is boolean. If true, the command's invocation string is logged.
+
"""
if log:
- log.info(command)
+ LOG.info(command)
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory)
out, err = p.communicate()
return (out, err)
@@ -36,6 +39,7 @@ def create_dir_if_necessary(pathname):
def remove_file(filename, log=LOG, verbose=True):
"""
Attempt to delete filename.
+ log is boolean. If true, removal is logged.
Log a warning if file does not exist.
Logging filenames are releative to BASE_DIR to cut down on noise in output.
"""
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 67057a30e7..7d970c8de2 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -28,7 +28,7 @@ def validate_po_file(filename, log):
raise SkipTest()
# Use relative paths to make output less noisy.
rfile = os.path.relpath(filename, LOCALE_DIR)
- (out, err) = call(['msgfmt','-c', rfile], log=None, working_directory=LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], log=False, working_directory=LOCALE_DIR)
if err != '':
log.warn('\n'+err)
From 70cca0540dbccf086a121d973c1dd7ba30f46b5d Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 15:23:28 -0400
Subject: [PATCH 050/123] reorder imports for pep8
---
cms/djangoapps/contentstore/views/preview.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index 36ca01ec86..bbee9f621c 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,5 +1,4 @@
import logging, sys
-import static_replace
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
@@ -17,6 +16,7 @@ from xmodule.modulestore.mongo import MongoUsage
from xmodule.x_module import ModuleSystem
from xblock.runtime import DbModel
+import static_replace
from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from access import has_access
From 571a9af0c087eae413001db91ea5fdc25d01eb46 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 16:37:28 -0400
Subject: [PATCH 051/123] declared exports
---
cms/djangoapps/contentstore/views/__init__.py | 19 ++++++++++---------
cms/djangoapps/contentstore/views/assets.py | 2 ++
.../contentstore/views/checklist.py | 2 ++
cms/djangoapps/contentstore/views/error.py | 2 ++
cms/djangoapps/contentstore/views/item.py | 2 ++
cms/djangoapps/contentstore/views/preview.py | 2 ++
cms/djangoapps/contentstore/views/public.py | 2 ++
cms/djangoapps/contentstore/views/requests.py | 2 ++
cms/djangoapps/contentstore/views/tabs.py | 1 +
9 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index 37f786ac3c..e37e4bae37 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,13 +1,14 @@
# TODO: replace asterisks, should explicitly enumerate imports instead
-from assets import asset_index, upload_asset, import_course, generate_export_course, export_course
-from checklist import get_checklists, update_checklist
+from assets import *
+from checklist import *
from component import *
from course import *
-from error import not_found, server_error, render_404, render_500
-from item import save_item, clone_item, delete_item
-from preview import preview_dispatch, preview_component
-from public import signup, old_login_redirect, login_page, howitworks, ux_alerts
-from user import index, add_user, remove_user, manage_users
-from tabs import edit_tabs, reorder_static_tabs
-from requests import edge, event, landing
+from error import *
+from item import *
+from preview import *
+from public import *
+from user import *
+from tabs import *
+from requests import *
+
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index c2aa52b1e1..04eb0c0ed6 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -25,6 +25,8 @@ from xmodule.util.date_utils import get_default_time_display
from access import get_location_and_verify_access
from auth.authz import create_all_course_groups
+__all__ = ['asset_index', 'upload_asset', 'import_course', 'generate_export_course', 'export_course']
+
@login_required
@ensure_csrf_cookie
def asset_index(request, org, course, name):
diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py
index 4a97ddc1df..a86c751c14 100644
--- a/cms/djangoapps/contentstore/views/checklist.py
+++ b/cms/djangoapps/contentstore/views/checklist.py
@@ -12,6 +12,8 @@ from contentstore.utils import get_modulestore, get_url_reverse
from requests import get_request_method
from access import get_location_and_verify_access
+__all__ = ['get_checklists', 'update_checklist']
+
@ensure_csrf_cookie
@login_required
def get_checklists(request, org, course, name):
diff --git a/cms/djangoapps/contentstore/views/error.py b/cms/djangoapps/contentstore/views/error.py
index 814af96104..64d1538d5d 100644
--- a/cms/djangoapps/contentstore/views/error.py
+++ b/cms/djangoapps/contentstore/views/error.py
@@ -2,6 +2,8 @@ from django.http import HttpResponseServerError, HttpResponseNotFound
from mitxmako.shortcuts import render_to_string, render_to_response
+__all__ = ['not_found', 'server_error', 'render_404', 'render_500']
+
def not_found(request):
return render_to_response('error.html', {'error': '404'})
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index b6d03e3f81..ac82e38577 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -14,6 +14,8 @@ from contentstore.utils import get_modulestore
from access import has_access
from requests import _xmodule_recurse
+__all__ = ['save_item', 'clone_item', 'delete_item']
+
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index bbee9f621c..5e8abb238c 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -21,6 +21,8 @@ from session_kv_store import SessionKeyValueStore
from requests import render_from_lms
from access import has_access
+__all__ = ['preview_dispatch', 'preview_component']
+
log = logging.getLogger(__name__)
@login_required
diff --git a/cms/djangoapps/contentstore/views/public.py b/cms/djangoapps/contentstore/views/public.py
index 3ab9e4e5a0..fe8a2b7a91 100644
--- a/cms/djangoapps/contentstore/views/public.py
+++ b/cms/djangoapps/contentstore/views/public.py
@@ -8,6 +8,8 @@ from mitxmako.shortcuts import render_to_response
from external_auth.views import ssl_login_shortcut
from user import index
+__all__ = ['signup', 'old_login_redirect', 'login_page', 'howitworks', 'ux_alerts']
+
"""
Public views
"""
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 58a3275527..46b42fad7a 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -3,6 +3,8 @@ import json
from django.http import HttpResponse
from mitxmako.shortcuts import render_to_string, render_to_response
+__all__ = ['edge', 'event', 'landing']
+
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
diff --git a/cms/djangoapps/contentstore/views/tabs.py b/cms/djangoapps/contentstore/views/tabs.py
index 9a6d8736bf..b947c163eb 100644
--- a/cms/djangoapps/contentstore/views/tabs.py
+++ b/cms/djangoapps/contentstore/views/tabs.py
@@ -12,6 +12,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_course_for_item
+__all__ = ['edit_tabs', 'reorder_static_tabs']
def initialize_course_tabs(course):
# set up the default tabs
From 408e26a0c1921fc8d9a45416b8886c28d455edc2 Mon Sep 17 00:00:00 2001
From: Jason Bau
Date: Thu, 9 May 2013 16:21:55 -0700
Subject: [PATCH 052/123] parameterizes CMS email settings
---
cms/envs/aws.py | 10 ++++++++++
cms/envs/common.py | 1 +
2 files changed, 11 insertions(+)
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index 59ad8b835e..05c57d8263 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -43,6 +43,16 @@ CACHES = ENV_TOKENS['CACHES']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
+#Email overrides
+DEFAULT_FROM_EMAIL = ENV_TOKENS.get('DEFAULT_FROM_EMAIL', DEFAULT_FROM_EMAIL)
+DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBACK_EMAIL)
+ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
+SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
+
+#Timezone overrides
+TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
+
+
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 8effc773e0..a53830082b 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -152,6 +152,7 @@ IGNORABLE_404_ENDS = ('favicon.ico')
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'registration@edx.org'
DEFAULT_FEEDBACK_EMAIL = 'feedback@edx.org'
+SERVER_EMAIL = 'devops@edx.org'
ADMINS = (
('edX Admins', 'admin@edx.org'),
)
From c071ee448f69aa409f3898ea19f842d05cc60881 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Fri, 10 May 2013 16:51:26 -0400
Subject: [PATCH 053/123] pep8 compliance
---
cms/djangoapps/contentstore/views/__init__.py | 2 --
cms/djangoapps/contentstore/views/component.py | 2 ++
cms/djangoapps/contentstore/views/course.py | 4 +++-
cms/djangoapps/contentstore/views/preview.py | 9 ++++++---
cms/djangoapps/contentstore/views/requests.py | 1 -
5 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index e37e4bae37..e17b27e9b1 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -1,5 +1,3 @@
-# TODO: replace asterisks, should explicitly enumerate imports instead
-
from assets import *
from checklist import *
from component import *
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 2dd7307976..f2a63c9b2c 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -25,6 +25,8 @@ from models.settings.course_grading import CourseGradingModel
from requests import get_request_method, _xmodule_recurse
from access import has_access, get_location_and_verify_access
+# TODO: should explicitly enumerate exports with __all__
+
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
log = logging.getLogger(__name__)
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index c0f6acc808..336a4ad0fe 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -17,13 +17,15 @@ from contentstore.utils import get_lms_link_for_item, add_open_ended_panel_tab,
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
-from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from auth.authz import create_all_course_groups
from util.json_request import expect_json
+
from access import has_access, get_location_and_verify_access
from requests import get_request_method
from tabs import initialize_course_tabs
+from component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
+# TODO: should explicitly enumerate exports with __all__
@login_required
@ensure_csrf_cookie
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index 5e8abb238c..0b839a6754 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -1,4 +1,5 @@
-import logging, sys
+import logging
+import sys
from functools import partial
from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
@@ -25,6 +26,7 @@ __all__ = ['preview_dispatch', 'preview_component']
log = logging.getLogger(__name__)
+
@login_required
def preview_dispatch(request, preview_id, location, dispatch=None):
"""
@@ -58,6 +60,7 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
return HttpResponse(ajax_return)
+
@login_required
def preview_component(request, location):
# TODO (vshnayder): change name from id to location in coffee+html as well.
@@ -72,7 +75,6 @@ def preview_component(request, location):
})
-
def preview_module_system(request, preview_id, descriptor):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
@@ -104,6 +106,7 @@ def preview_module_system(request, preview_id, descriptor):
xblock_model_data=preview_model_data,
)
+
def get_preview_module(request, preview_id, descriptor):
"""
Returns a preview XModule at the specified location. The preview_data is chosen arbitrarily
@@ -159,6 +162,7 @@ def load_preview_module(request, preview_id, descriptor):
return module
+
def get_module_previews(request, descriptor):
"""
Returns a list of preview XModule html contents. One preview is returned for each
@@ -171,4 +175,3 @@ def get_module_previews(request, descriptor):
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
-
diff --git a/cms/djangoapps/contentstore/views/requests.py b/cms/djangoapps/contentstore/views/requests.py
index 46b42fad7a..07903637d3 100644
--- a/cms/djangoapps/contentstore/views/requests.py
+++ b/cms/djangoapps/contentstore/views/requests.py
@@ -40,7 +40,6 @@ def create_json_response(errmsg=None):
resp = HttpResponse(json.dumps({'Status': 'Failed', 'ErrMsg': errmsg}))
else:
resp = HttpResponse(json.dumps({'Status': 'OK'}))
-
return resp
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
From d95e87cf6734ae0eede773bdd61ccf4e5f24db65 Mon Sep 17 00:00:00 2001
From: Chris Dodge
Date: Sat, 11 May 2013 10:34:36 -0400
Subject: [PATCH 054/123] insert the textbook XML element when writing
definition to xml
---
common/lib/xmodule/xmodule/course_module.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 5efd7b4005..2a34b75a1f 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -382,6 +382,19 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return definition, children
+ def definition_to_xml(self, resource_fs):
+ xml_object = super(CourseDescriptor, self).definition_to_xml(resource_fs)
+
+ if len(self.textbooks) > 0:
+ textbook_xml_object = etree.Element('textbook')
+ for textbook in self.textbooks:
+ textbook_xml_object.set('title', textbook.title)
+ textbook_xml_object.set('book_url', textbook.book_url)
+
+ xml_object.append(textbook_xml_object)
+
+ return xml_object
+
def has_ended(self):
"""
Returns True if the current time is after the specified course end date.
From 317831a8132e432723898324cd119e61fe4a01d0 Mon Sep 17 00:00:00 2001
From: "Mark L. Chang"
Date: Fri, 10 May 2013 13:21:09 -0400
Subject: [PATCH 055/123] copy change
---
cms/templates/500.html | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/cms/templates/500.html b/cms/templates/500.html
index 2645b0067b..6e381c0844 100644
--- a/cms/templates/500.html
+++ b/cms/templates/500.html
@@ -1,12 +1,17 @@
<%inherit file="base.html" />
-<%block name="title">Server Error%block>
+
+<%block name="title">Studio Server Error%block>
<%block name="content">
-
Currently the edX servers are down
-
Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.
+
The Studio servers encountered an error
+
+ An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
+ We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ If the problem persists, please email us at technical@edx.org.
+
An error occurred in Studio and the page could not be loaded. Please try again in a few moments.
- We've logged the error and our staff is currently working to get Studio back up as soon as possible.
+ We've logged the error and our staff is currently working to resolve this error as soon as possible.
If the problem persists, please email us at technical@edx.org.
From 1f7bf1f00a43ccf142b1f4b1517cbeb2acbcafd3 Mon Sep 17 00:00:00 2001
From: Steve Strassmann
Date: Sun, 12 May 2013 21:52:07 -0400
Subject: [PATCH 057/123] use global LOG instead of local log
---
i18n/execute.py | 18 +++++++-----------
i18n/extract.py | 5 +++--
i18n/generate.py | 12 ++++++------
i18n/tests/test_validate.py | 2 +-
4 files changed, 17 insertions(+), 20 deletions(-)
diff --git a/i18n/execute.py b/i18n/execute.py
index 1ff439ef38..8e7f0f52de 100644
--- a/i18n/execute.py
+++ b/i18n/execute.py
@@ -4,28 +4,24 @@ from config import CONFIGURATION, BASE_DIR
LOG = logging.getLogger(__name__)
-def execute(command, working_directory=BASE_DIR, log=True):
+def execute(command, working_directory=BASE_DIR):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
- log is boolean. If true, the command's invocation string is logged.
Output is ignored.
"""
- if log:
- LOG.info(command)
+ LOG.info(command)
subprocess.call(command.split(' '), cwd=working_directory)
-def call(command, working_directory=BASE_DIR, log=True):
+def call(command, working_directory=BASE_DIR):
"""
Executes shell command in a given working_directory.
Command is a string to pass to the shell.
Returns a tuple of two strings: (stdout, stderr)
- log is boolean. If true, the command's invocation string is logged.
"""
- if log:
- LOG.info(command)
+ LOG.info(command)
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory)
out, err = p.communicate()
return (out, err)
@@ -36,7 +32,7 @@ def create_dir_if_necessary(pathname):
os.makedirs(dirname)
-def remove_file(filename, log=LOG, verbose=True):
+def remove_file(filename, verbose=True):
"""
Attempt to delete filename.
log is boolean. If true, removal is logged.
@@ -44,8 +40,8 @@ def remove_file(filename, log=LOG, verbose=True):
Logging filenames are releative to BASE_DIR to cut down on noise in output.
"""
if verbose:
- log.info('Deleting file %s' % os.path.relpath(filename, BASE_DIR))
+ LOG.info('Deleting file %s' % os.path.relpath(filename, BASE_DIR))
if not os.path.exists(filename):
- log.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
+ LOG.warn("File does not exist: %s" % os.path.relpath(filename, BASE_DIR))
else:
os.remove(filename)
diff --git a/i18n/extract.py b/i18n/extract.py
index c517de3b51..c28c3868e2 100755
--- a/i18n/extract.py
+++ b/i18n/extract.py
@@ -32,8 +32,9 @@ BABEL_OUT = BASE_DIR.relpathto(CONFIGURATION.source_messages_dir.joinpath('mako.
SOURCE_WARN = 'This English source file is machine-generated. Do not check it into github'
+LOG = logging.getLogger(__name__)
+
def main ():
- log = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
create_dir_if_necessary(LOCALE_DIR)
source_msgs_dir = CONFIGURATION.source_messages_dir
@@ -63,7 +64,7 @@ def main ():
execute(make_djangojs_cmd, working_directory=BASE_DIR)
for filename in generated_files:
- log.info('Cleaning %s' % filename)
+ LOG.info('Cleaning %s' % filename)
po = pofile(source_msgs_dir.joinpath(filename))
# replace default headers with edX headers
fix_header(po)
diff --git a/i18n/generate.py b/i18n/generate.py
index 48470796a2..65c65c00d6 100755
--- a/i18n/generate.py
+++ b/i18n/generate.py
@@ -17,9 +17,11 @@ import os, sys, logging
from polib import pofile
from config import BASE_DIR, CONFIGURATION
-from execute import execute, remove_file
+from execute import execute
-def merge(locale, target='django.po', fail_if_missing=True, log=None):
+LOG = logging.getLogger(__name__)
+
+def merge(locale, target='django.po', fail_if_missing=True):
"""
For the given locale, merge django-partial.po, messages.po, mako.po -> django.po
target is the resulting filename
@@ -28,8 +30,7 @@ def merge(locale, target='django.po', fail_if_missing=True, log=None):
If fail_if_missing is False, and the files to be merged are missing,
just return silently.
"""
- if log:
- log.info('Merging locale={0}'.format(locale))
+ LOG.info('Merging locale={0}'.format(locale))
locale_directory = CONFIGURATION.get_messages_dir(locale)
files_to_merge = ('django-partial.po', 'messages.po', 'mako.po')
try:
@@ -71,13 +72,12 @@ def validate_files(dir, files_to_merge):
raise Exception("I18N: Cannot generate because file not found: {0}".format(pathname))
def main ():
- log = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
for locale in CONFIGURATION.locales:
merge(locale)
# Dummy text is not required. Don't raise exception if files are missing.
- merge(CONFIGURATION.dummy_locale, fail_if_missing=False, log=log)
+ merge(CONFIGURATION.dummy_locale, fail_if_missing=False)
compile_cmd = 'django-admin.py compilemessages'
execute(compile_cmd, working_directory=BASE_DIR)
diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py
index 7d970c8de2..bef563faea 100644
--- a/i18n/tests/test_validate.py
+++ b/i18n/tests/test_validate.py
@@ -28,7 +28,7 @@ def validate_po_file(filename, log):
raise SkipTest()
# Use relative paths to make output less noisy.
rfile = os.path.relpath(filename, LOCALE_DIR)
- (out, err) = call(['msgfmt','-c', rfile], log=False, working_directory=LOCALE_DIR)
+ (out, err) = call(['msgfmt','-c', rfile], working_directory=LOCALE_DIR)
if err != '':
log.warn('\n'+err)
From e5daeb41fb7daaf03169a8fd05e7221d76537df8 Mon Sep 17 00:00:00 2001
From: Alexander Kryklia
Date: Thu, 4 Apr 2013 12:33:22 +0300
Subject: [PATCH 058/123] update docstring
---
common/lib/xmodule/xmodule/poll_module.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py
index c8ad44a918..dafcef9835 100644
--- a/common/lib/xmodule/xmodule/poll_module.py
+++ b/common/lib/xmodule/xmodule/poll_module.py
@@ -4,8 +4,6 @@ to do set of polls.
On the client side we show:
If student does not yet anwered - Question with set of choices.
If student have answered - Question with statistics for each answers.
-
-Student can't change his answer.
"""
import cgi
From bb9bc421713b8f91e807fe156b0fe2afea327ea6 Mon Sep 17 00:00:00 2001
From: Alexander Kryklia
Date: Thu, 4 Apr 2013 12:34:03 +0300
Subject: [PATCH 059/123] adds initial word_cloud_module files
---
common/lib/xmodule/setup.py | 1 +
.../xmodule/js/src/word_cloud/logme.js | 54 +++
.../xmodule/js/src/word_cloud/word_cloud.js | 323 ++++++++++++++++++
.../js/src/word_cloud/word_cloud_main.js | 5 +
.../lib/xmodule/xmodule/word_cloud_module.py | 166 +++++++++
lms/templates/word_cloud.html | 8 +
6 files changed, 557 insertions(+)
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
create mode 100644 common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
create mode 100644 common/lib/xmodule/xmodule/word_cloud_module.py
create mode 100644 lms/templates/word_cloud.html
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 982a77631d..43d970d898 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -52,6 +52,7 @@ setup(
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
+ "word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
],
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js b/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
new file mode 100644
index 0000000000..c045757044
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/logme.js
@@ -0,0 +1,54 @@
+// Wrapper for RequireJS. It will make the standard requirejs(), require(), and
+// define() functions from Require JS available inside the anonymous function.
+(function (requirejs, require, define) {
+
+define('logme', [], function () {
+ var debugMode;
+
+ // debugMode can be one of the following:
+ //
+ // true - All messages passed to logme will be written to the internal
+ // browser console.
+ // false - Suppress all output to the internal browser console.
+ //
+ // Obviously, if anywhere there is a direct console.log() call, we can't do
+ // anything about it. That's why use logme() - it will allow to turn off
+ // the output of debug information with a single change to a variable.
+ debugMode = true;
+
+ return logme;
+
+ /*
+ * function: logme
+ *
+ * A helper function that provides logging facilities. We don't want
+ * to call console.log() directly, because sometimes it is not supported
+ * by the browser. Also when everything is routed through this function.
+ * the logging output can be easily turned off.
+ *
+ * logme() supports multiple parameters. Each parameter will be passed to
+ * console.log() function separately.
+ *
+ */
+ function logme() {
+ var i;
+
+ if (
+ (typeof debugMode === 'undefined') ||
+ (debugMode !== true) ||
+ (typeof window.console === 'undefined')
+ ) {
+ return;
+ }
+
+ for (i = 0; i < arguments.length; i++) {
+ window.console.log(arguments[i]);
+ }
+ } // End-of: function logme
+});
+
+// End of wrapper for RequireJS. As you can see, we are passing
+// namespaced Require JS variables to an anonymous function. Within
+// it, you can use the standard requirejs(), require(), and define()
+// functions as if they were in the global namespace.
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define)
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
new file mode 100644
index 0000000000..74f2a488d7
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud.js
@@ -0,0 +1,323 @@
+(function (requirejs, require, define) {
+define('PollMain', ['logme'], function (logme) {
+
+PollMain.prototype = {
+
+'showAnswerGraph': function (poll_answers, total) {
+ var _this, totalValue;
+
+ totalValue = parseFloat(total);
+ if (isFinite(totalValue) === false) {
+ return;
+ }
+
+ _this = this;
+
+ $.each(poll_answers, function (index, value) {
+ var numValue, percentValue;
+
+ numValue = parseFloat(value);
+ if (isFinite(numValue) === false) {
+ return;
+ }
+
+ percentValue = (numValue / totalValue) * 100.0;
+
+ _this.answersObj[index].statsEl.show();
+ _this.answersObj[index].numberEl.html('' + value + ' (' + percentValue.toFixed(1) + '%)');
+ _this.answersObj[index].percentEl.css({
+ 'width': '' + percentValue.toFixed(1) + '%'
+ });
+ });
+},
+
+'submitAnswer': function (answer, answerObj) {
+ var _this;
+
+ // Make sure that the user can answer a question only once.
+ if (this.questionAnswered === true) {
+ return;
+ }
+ this.questionAnswered = true;
+
+ _this = this;
+
+ console.log('submit answer');
+
+ answerObj.buttonEl.addClass('answered');
+
+ // Send the data to the server as an AJAX request. Attach a callback that will
+ // be fired on server's response.
+ $.postWithPrefix(
+ _this.ajax_url + '/' + answer, {},
+ function (response) {
+ console.log('success! response = ');
+ console.log(response);
+
+ _this.showAnswerGraph(response.poll_answers, response.total);
+
+ if (_this.canReset === true) {
+ _this.resetButton.show();
+ }
+
+ // Initialize Conditional constructors.
+ if (_this.wrapperSectionEl !== null) {
+ $(_this.wrapperSectionEl).find('.xmodule_ConditionalModule').each(function (index, value) {
+ new window.Conditional(value, _this.id.replace(/^poll_/, ''));
+ });
+ }
+ }
+ );
+
+}, // End-of: 'submitAnswer': function (answer, answerEl) {
+
+
+'submitReset': function () {
+ var _this;
+
+ _this = this;
+
+ console.log('submit reset');
+
+ // Send the data to the server as an AJAX request. Attach a callback that will
+ // be fired on server's response.
+ $.postWithPrefix(
+ this.ajax_url + '/' + 'reset_poll',
+ {},
+ function (response) {
+ console.log('success! response = ');
+ console.log(response);
+
+ if (
+ (response.hasOwnProperty('status') !== true) ||
+ (typeof response.status !== 'string') ||
+ (response.status.toLowerCase() !== 'success')) {
+ return;
+ }
+
+ _this.questionAnswered = false;
+ _this.questionEl.find('.button.answered').removeClass('answered');
+ _this.questionEl.find('.stats').hide();
+ _this.resetButton.hide();
+
+ // Initialize Conditional constructors. We will specify the third parameter as 'true'
+ // notifying the constructor that this is a reset operation.
+ if (_this.wrapperSectionEl !== null) {
+ $(_this.wrapperSectionEl).find('.xmodule_ConditionalModule').each(function (index, value) {
+ new window.Conditional(value, _this.id.replace(/^poll_/, ''));
+ });
+ }
+ }
+ );
+}, // End-of: 'submitAnswer': function (answer, answerEl) {
+
+'postInit': function () {
+ var _this;
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ if (
+ (this.jsonConfig.poll_answer.length > 0) &&
+ (this.jsonConfig.answers.hasOwnProperty(this.jsonConfig.poll_answer) === false)
+ ) {
+ this.questionEl.append(
+ '
Error!
' +
+ '
XML data format changed. List of answers was modified, but poll data was not updated.
'
+ );
+
+ return;
+ }
+
+ // Get the DOM id of the question.
+ this.id = this.questionEl.attr('id');
+
+ // Get the URL to which we will post the users answer to the question.
+ this.ajax_url = this.questionEl.data('ajax-url');
+
+ this.questionHtmlMarkup = $('').html(this.jsonConfig.question).text();
+ this.questionEl.append(this.questionHtmlMarkup);
+
+ // When the user selects and answer, we will set this flag to true.
+ this.questionAnswered = false;
+
+ this.answersObj = {};
+ this.shortVersion = true;
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ if (value.length >= 18) {
+ _this.shortVersion = false;
+ }
+ });
+
+ $.each(this.jsonConfig.answers, function (index, value) {
+ var answer;
+
+ answer = {};
+
+ _this.answersObj[index] = answer;
+
+ answer.el = $('');
+
+ answer.questionEl = $('');
+ answer.buttonEl = $('');
+ answer.textEl = $('');
+ answer.questionEl.append(answer.buttonEl);
+ answer.questionEl.append(answer.textEl);
+
+ answer.el.append(answer.questionEl);
+
+ answer.statsEl = $('');
+ answer.barEl = $('');
+ answer.percentEl = $('');
+ answer.barEl.append(answer.percentEl);
+ answer.numberEl = $('');
+ answer.statsEl.append(answer.barEl);
+ answer.statsEl.append(answer.numberEl);
+
+ answer.statsEl.hide();
+
+ answer.el.append(answer.statsEl);
+
+ answer.textEl.html(value);
+
+ if (_this.shortVersion === true) {
+ $.each(answer, function (index, value) {
+ if (value instanceof jQuery) {
+ value.addClass('short');
+ }
+ });
+ }
+
+ answer.el.appendTo(_this.questionEl);
+
+ answer.textEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ answer.buttonEl.on('click', function () {
+ _this.submitAnswer(index, answer);
+ });
+
+ if (index === _this.jsonConfig.poll_answer) {
+ answer.buttonEl.addClass('answered');
+ _this.questionAnswered = true;
+ }
+ });
+
+ console.log(this.jsonConfig.reset);
+
+ if ((typeof this.jsonConfig.reset === 'string') && (this.jsonConfig.reset.toLowerCase() === 'true')) {
+ this.canReset = true;
+
+ this.resetButton = $('
Change your vote
');
+
+ if (this.questionAnswered === false) {
+ this.resetButton.hide();
+ }
+
+ this.resetButton.appendTo(this.questionEl);
+
+ this.resetButton.on('click', function () {
+ _this.submitReset();
+ });
+ } else {
+ this.canReset = false;
+ }
+
+ // If it turns out that the user already answered the question, show the answers graph.
+ if (this.questionAnswered === true) {
+ this.showAnswerGraph(this.jsonConfig.poll_answers, this.jsonConfig.total);
+ }
+} // End-of: 'postInit': function () {
+}; // End-of: PollMain.prototype = {
+
+return PollMain;
+
+function PollMain(el) {
+ var _this;
+
+ this.questionEl = $(el).find('.poll_question');
+ if (this.questionEl.length !== 1) {
+ // We require one question DOM element.
+ logme('ERROR: PollMain constructor requires one question DOM element.');
+
+ return;
+ }
+
+ // Just a safety precussion. If we run this code more than once, multiple 'click' callback handlers will be
+ // attached to the same DOM elements. We don't want this to happen.
+ if (this.questionEl.attr('poll_main_processed') === 'true') {
+ logme(
+ 'ERROR: PolMain JS constructor was called on a DOM element that has already been processed once.'
+ );
+
+ return;
+ }
+
+ // This element was not processed earlier.
+ // Make sure that next time we will not process this element a second time.
+ this.questionEl.attr('poll_main_processed', 'true');
+
+ // Access this object inside inner functions.
+ _this = this;
+
+ // DOM element which contains the current poll along with any conditionals. By default we assume that such
+ // element is not present. We will try to find it.
+ this.wrapperSectionEl = null;
+
+ (function (tempEl, c1) {
+ while (tempEl.tagName.toLowerCase() !== 'body') {
+ tempEl = $(tempEl).parent()[0];
+ c1 += 1;
+
+ if (
+ (tempEl.tagName.toLowerCase() === 'section') &&
+ ($(tempEl).hasClass('xmodule_WrapperModule') === true)
+ ) {
+ _this.wrapperSectionEl = tempEl;
+
+ break;
+ } else if (c1 > 50) {
+ // In case something breaks, and we enter an endless loop, a sane
+ // limit for loop iterations.
+
+ break;
+ }
+ }
+ }($(el)[0], 0));
+
+ try {
+ this.jsonConfig = JSON.parse(this.questionEl.children('.poll_question_div').html());
+
+ $.postWithPrefix(
+ '' + this.questionEl.data('ajax-url') + '/' + 'get_state', {},
+ function (response) {
+ _this.jsonConfig.poll_answer = response.poll_answer;
+ _this.jsonConfig.total = response.total;
+
+ $.each(response.poll_answers, function (index, value) {
+ _this.jsonConfig.poll_answers[index] = value;
+ });
+
+ _this.questionEl.children('.poll_question_div').html(JSON.stringify(_this.jsonConfig));
+
+ _this.postInit();
+ }
+ );
+
+ return;
+ } catch (err) {
+ logme(
+ 'ERROR: Invalid JSON config for poll ID "' + this.id + '".',
+ 'Error messsage: "' + err.message + '".'
+ );
+
+ return;
+ }
+} // End-of: function PollMain(el) {
+
+}); // End-of: define('PollMain', ['logme'], function (logme) {
+
+// End-of: (function (requirejs, require, define) {
+}(RequireJS.requirejs, RequireJS.require, RequireJS.define));
diff --git a/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
new file mode 100644
index 0000000000..a2ccbc7c03
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/src/word_cloud/word_cloud_main.js
@@ -0,0 +1,5 @@
+window.Poll = function (el) {
+ RequireJS.require(['PollMain'], function (PollMain) {
+ new PollMain(el);
+ });
+};
diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py
new file mode 100644
index 0000000000..7c2dfc6ad2
--- /dev/null
+++ b/common/lib/xmodule/xmodule/word_cloud_module.py
@@ -0,0 +1,166 @@
+"""Word cloud is ungraded xblock used by students to
+generate and view word cloud..
+
+On the client side we show:
+If student does not yet anwered - five text inputs.
+If student have answered - words he entered and cloud.
+
+Stunent can change his answer.
+"""
+
+import cgi
+import json
+import logging
+from copy import deepcopy
+from collections import OrderedDict
+
+from lxml import etree
+from pkg_resources import resource_string
+
+from xmodule.x_module import XModule
+from xmodule.stringify import stringify_children
+from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.xml_module import XmlDescriptor
+from xblock.core import Scope, String, Object, Boolean, List
+
+log = logging.getLogger(__name__)
+
+
+class WordCloudFields(object):
+ # Name of poll to use in links to this poll
+ display_name = String(help="Display name for this module", scope=Scope.settings)
+
+ submitted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
+ student_words= List(help="Student answer", scope=Scope.student_state, default=[])
+ all_words = Object(help="All possible words from other students", scope=Scope.content)
+ top_words = Object(help="Top N words for word cloud", scope=Scope.content)
+ top_low_border = Int(help="Number to distinguish top from all words", scope=Scope.content)
+
+class WordCloudModule(WordCloudFields, XModule):
+ """WordCloud Module"""
+ js = {
+ 'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee')],
+ 'js': [resource_string(__name__, 'js/src/word_cloud/logme.js'),
+ resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
+ resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
+ }
+ css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
+ js_module_name = "Word_Cloud"
+
+ Number_of_top_words = 250
+
+ def handle_ajax(self, dispatch, get):
+ """Ajax handler.
+
+ Args:
+ dispatch: string request slug
+ get: dict request get parameters
+
+ Returns:
+ json string
+ """
+ if dispatch == 'submit':
+
+ # self.all_words[word] -= 1
+ # FIXME: fix this, when xblock will support mutable types.
+ # Now we use this hack.
+ # speed issues
+ temp_all_words = self.all_words
+ temp_top_words = self.top_words
+
+ if self.submitted:
+
+ for word in self.student_words:
+ temp_all_words[word] -= 1
+
+ if word in temp_top_words:
+ temp_top_words -= 1
+
+ else:
+ self.submitted = True
+
+ self.student_words = get['student_words']
+
+ question_words = {}
+
+ for word in self.student_words:
+ temp_all_words[word] += 1
+
+ if word in temp_top_words:
+ temp_top_words += 1
+ else:
+ if temp_all_words[word] > top_low_border:
+ question_words[word] = temp_all_words[word]
+
+
+ self.all_words = temp_all_words
+
+ self.top_words = self.update_top_words(question_words, temp_top_words)
+
+
+ return json.dumps({'student_words': self.student_words,
+ 'top_words': self.top_words,
+ })
+ elif dispatch == 'get_state':
+ return json.dumps({'student_answers': self.student_answers,
+ 'top_words': self.top_words)
+ })
+ else: # return error message
+ return json.dumps({'error': 'Unknown Command!'})
+
+
+ def update_top_words(question_words, top_words):
+
+ for word, number in question_words:
+ for top_word, top_number in top_words[:]:
+ if top_number < number:
+ del top_words[top_word]
+ top_words[word] - number
+ break
+
+ return top_words
+
+ def get_html(self):
+ """Renders parameters to template."""
+ params = {
+ 'element_id': self.location.html_id(),
+ 'element_class': self.location.category,
+ 'ajax_url': self.system.ajax_url,
+ 'configuration_json': json.dumps({}),
+ }
+ self.content = self.system.render_template('word_cloud.html', params)
+ return self.content
+
+
+
+class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
+ _tag_name = 'word_cloud'
+
+ module_class = WordCloudModule
+ template_dir_name = 'word_cloud'
+ stores_state = True
+
+ @classmethod
+ def definition_from_xml(cls, xml_object, system):
+ """Pull out the data into dictionary.
+
+ Args:
+ xml_object: xml from file.
+ system: `system` object.
+
+ Returns:
+ (definition, children) - tuple
+
+ """
+ definition = {}
+ children = []
+
+ return (definition, children)
+
+ def definition_to_xml(self, resource_fs):
+ """Return an xml element representing to this definition."""
+ poll_str = '<{tag_name}/>'.format(tag_name=self._tag_name)
+ xml_object = etree.fromstring(poll_str)
+ xml_object.set('display_name', self.display_name)
+
+ return xml_object
diff --git a/lms/templates/word_cloud.html b/lms/templates/word_cloud.html
new file mode 100644
index 0000000000..091d2b0317
--- /dev/null
+++ b/lms/templates/word_cloud.html
@@ -0,0 +1,8 @@
+
+
+
${configuration_json}
+
From 143d2c86367f99ec82342f68c8071714b29e4477 Mon Sep 17 00:00:00 2001
From: Vasyl Nakvasiuk
Date: Fri, 5 Apr 2013 16:13:05 +0300
Subject: [PATCH 060/123] fix WordCloudDescriptor
---
common/lib/xmodule/xmodule/word_cloud_module.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py
index 7c2dfc6ad2..63c8d40f4a 100644
--- a/common/lib/xmodule/xmodule/word_cloud_module.py
+++ b/common/lib/xmodule/xmodule/word_cloud_module.py
@@ -21,7 +21,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
-from xblock.core import Scope, String, Object, Boolean, List
+from xblock.core import Scope, String, Object, Boolean, List, Integer
log = logging.getLogger(__name__)
@@ -29,12 +29,13 @@ log = logging.getLogger(__name__)
class WordCloudFields(object):
# Name of poll to use in links to this poll
display_name = String(help="Display name for this module", scope=Scope.settings)
+ num_inputs = Integer(help="Number of inputs", scope=Scope.settings)
submitted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False)
student_words= List(help="Student answer", scope=Scope.student_state, default=[])
all_words = Object(help="All possible words from other students", scope=Scope.content)
top_words = Object(help="Top N words for word cloud", scope=Scope.content)
- top_low_border = Int(help="Number to distinguish top from all words", scope=Scope.content)
+ top_low_border = Integer(help="Number to distinguish top from all words", scope=Scope.content)
class WordCloudModule(WordCloudFields, XModule):
"""WordCloud Module"""
@@ -44,8 +45,8 @@ class WordCloudModule(WordCloudFields, XModule):
resource_string(__name__, 'js/src/word_cloud/word_cloud.js'),
resource_string(__name__, 'js/src/word_cloud/word_cloud_main.js')]
}
- css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
- js_module_name = "Word_Cloud"
+ # css = {'scss': [resource_string(__name__, 'css/word_cloud/display.scss')]}
+ js_module_name = "WordCloud"
Number_of_top_words = 250
@@ -103,7 +104,7 @@ class WordCloudModule(WordCloudFields, XModule):
})
elif dispatch == 'get_state':
return json.dumps({'student_answers': self.student_answers,
- 'top_words': self.top_words)
+ 'top_words': self.top_words
})
else: # return error message
return json.dumps({'error': 'Unknown Command!'})
@@ -132,7 +133,6 @@ class WordCloudModule(WordCloudFields, XModule):
return self.content
-
class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
_tag_name = 'word_cloud'
@@ -159,8 +159,9 @@ class WordCloudDescriptor(WordCloudFields, MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs):
"""Return an xml element representing to this definition."""
- poll_str = '<{tag_name}/>'.format(tag_name=self._tag_name)
- xml_object = etree.fromstring(poll_str)
+ xml_str = '<{tag_name} />'.format(tag_name=self._tag_name)
+ xml_object = etree.fromstring(xml_str)
xml_object.set('display_name', self.display_name)
+ xml_object.set('num_inputs', self.num_inputs)
return xml_object
From 4b3fe54d48fc6febc3dcb7139623a904ada3ab65 Mon Sep 17 00:00:00 2001
From: Vasyl Nakvasiuk
Date: Fri, 5 Apr 2013 18:01:35 +0300
Subject: [PATCH 061/123] add test data xml for word_cloud
---
.../lib/xmodule/xmodule/tests/test_export.py | 3 +
common/test/data/word_cloud/README | 50 +++++++++++
common/test/data/word_cloud/README.md | 2 +
.../about/2013_Spring/overview.html | 79 ++++++++++++++++++
.../about/2013_Spring/prerequisites.html | 1 +
.../about/2013_Spring/short_description.html | 1 +
.../word_cloud/about/2013_Spring/video.html | 1 +
common/test/data/word_cloud/chapter/Staff.xml | 3 +
.../data/word_cloud/conditional/condone.xml | 3 +
common/test/data/word_cloud/course.xml | 2 +
.../data/word_cloud/course/2013_Spring.xml | 6 ++
.../test/data/word_cloud/creating_course.xml | 8 ++
.../word_cloud/info/2013_Spring/handouts.html | 3 +
.../word_cloud/info/2013_Spring/updates.html | 10 +++
.../policies/2013_Spring/policy.json | 8 ++
.../data/word_cloud/roots/2013_Spring.xml | 2 +
.../word_cloud/sequential/Problem_Demos.xml | 9 ++
common/test/data/word_cloud/static/README | 5 ++
.../word_cloud/static/images/course_image.jpg | Bin 0 -> 12626 bytes
.../static/images/professor-sandel.jpg | Bin 0 -> 453715 bytes
20 files changed, 196 insertions(+)
create mode 100644 common/test/data/word_cloud/README
create mode 100644 common/test/data/word_cloud/README.md
create mode 100644 common/test/data/word_cloud/about/2013_Spring/overview.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/prerequisites.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/short_description.html
create mode 100644 common/test/data/word_cloud/about/2013_Spring/video.html
create mode 100644 common/test/data/word_cloud/chapter/Staff.xml
create mode 100644 common/test/data/word_cloud/conditional/condone.xml
create mode 100644 common/test/data/word_cloud/course.xml
create mode 100644 common/test/data/word_cloud/course/2013_Spring.xml
create mode 100644 common/test/data/word_cloud/creating_course.xml
create mode 100644 common/test/data/word_cloud/info/2013_Spring/handouts.html
create mode 100644 common/test/data/word_cloud/info/2013_Spring/updates.html
create mode 100644 common/test/data/word_cloud/policies/2013_Spring/policy.json
create mode 100644 common/test/data/word_cloud/roots/2013_Spring.xml
create mode 100644 common/test/data/word_cloud/sequential/Problem_Demos.xml
create mode 100644 common/test/data/word_cloud/static/README
create mode 100644 common/test/data/word_cloud/static/images/course_image.jpg
create mode 100644 common/test/data/word_cloud/static/images/professor-sandel.jpg
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index 170a89d783..e912ce8a0d 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -123,3 +123,6 @@ class RoundTripTestCase(unittest.TestCase):
def test_exam_registration_roundtrip(self):
# Test exam_registration xmodule to see if it exports correctly
self.check_export_roundtrip(DATA_DIR, "test_exam_registration")
+
+ def test_word_cloud_roundtrip(self):
+ self.check_export_roundtrip(DATA_DIR, "word_cloud")
diff --git a/common/test/data/word_cloud/README b/common/test/data/word_cloud/README
new file mode 100644
index 0000000000..fc95a7c0c9
--- /dev/null
+++ b/common/test/data/word_cloud/README
@@ -0,0 +1,50 @@
+Any place that says "YEAR_SEMESTER" needs to be replaced with something
+in the form "2013_Spring". Take note of this name exactly, you'll need to
+use it everywhere, precisely - capitalization is very important.
+
+See https://github.com/MITx/mitx/blob/master/doc/xml-format.md for more on all this.
+-----------------------
+
+about/: Files that live here will be visible OUTSIDE OF COURSEWARE.
+ YEAR_SEMESTER/
+ end_date.html: Specifies in plain-text the end date of the course
+ overview.html: Text of the overview of the course
+ short_description.html: 10-15 words about the course
+ prerequisites.html: Any prerequisites for the course, or None if there are none.
+
+course/
+ YEAR_SEMESTER.xml: This is your top-level xml page that points at chapters.
+ Can just be for now.
+
+course.xml: This top level file points at a file in roots/. See creating_course.xml.
+
+creating_course.xml: Explains how to create course.xml
+
+info/: Files that live here will be visible on the COURSE LANDING PAGE
+ (Course Info) WITHIN THE COURSEWARE.
+ YEAR_SEMESTER/
+ handouts.html: A list of handouts, or an empty file if there are none
+ (if this file doesn't exist, it displays an error)
+ updates.html: Course updates.
+
+policies/
+ YEAR_SEMESTER/
+ policy.json: See https://github.com/MITx/mitx/blob/master/doc/xml-format.md
+ for more on the fields specified by this file.
+ grading_policy.json: Optional -- you don't need it to get a course off the
+ ground but will eventually. For more info see
+ https://github.com/MITx/mitx/blob/master/doc/course_grading.md
+
+roots/
+ YEAR_SEMESTER.xml: Looks something like
+
+ where ORG in {"MITx", "HarvardX", "BerkeleyX"}
+
+static/
+ See README.
+
+ images/
+ course_image.jpg: You MUST have an image named this to be the background
+ banner image on edx.org
+
+-----------------------
\ No newline at end of file
diff --git a/common/test/data/word_cloud/README.md b/common/test/data/word_cloud/README.md
new file mode 100644
index 0000000000..7dbfa46a26
--- /dev/null
+++ b/common/test/data/word_cloud/README.md
@@ -0,0 +1,2 @@
+content-harvard-justicex
+========================
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/overview.html b/common/test/data/word_cloud/about/2013_Spring/overview.html
new file mode 100644
index 0000000000..9c49899948
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/overview.html
@@ -0,0 +1,79 @@
+
+
+
+
About ER22x
+
+
Justice is a critical analysis of classical and contemporary theories of justice, including discussion of present-day applications. Topics include affirmative action, income distribution, same-sex marriage, the role of markets, debates about rights (human rights and property rights), arguments for and against equality, dilemmas of loyalty in public and private life. The course invites students to subject their own views on these controversies to critical examination.
+
+
The principle readings for the course are texts by Aristotle, John Locke, Immanuel Kant, John Stuart Mill, and John Rawls. Other assigned readings include writings by contemporary philosophers, court cases, and articles about political controversies that raise philosophical questions.
+
+
+
+
+
+
+
+
Course instructor
+
+
+
+
+
Michael J. Sandel
+
Michael J. Sandel is the Anne T. and Robert M. Bass Professor of Government at Harvard University, where he teaches political philosophy. His course "Justice" has enrolled more than 15,000 Harvard students. Sandel's writings have been published in 21 languages. His books include What Money Can't Buy: The Moral Limits of Markets (2012); Justice: What's the Right Thing to Do? (2009); The Case against Perfection: Ethics in the Age of Genetic Engineering (2007); Public Philosophy: Essays on Morality in Politics (2005); Democracy's Discontent (1996); and Liberalism and the Limits of Justice(1982; 2nd ed., 1998).
+
+
+
+
+
+
+
Frequently Asked Questions
+
+
How much does it cost to take the course?
+
Nothing! The course is free.
+
+
+
+
Does the course have any prerequisites?
+
No. Only an interest in thinking through some of the big ethical and civic questions we face in our everyday lives.
+
+
+
+
Do I need any other materials to take the course?
+
No. As long as you’ve got a computer to access the website, you are ready to take the course.
+
+
+
+
Is there a textbook for the course?
+
All of the course readings that are in the public domain are freely available online, at links provided on the course website. The course can be taken using these free resources alone. For those who wish to purchase a printed version of the assigned readings, an edited volume entitled, Justice: A Reader (ed., Michael Sandel) is available in paperback from Oxford University Press (in bookstores and from online booksellers). Those who would like supplementary readings on the themes of the lectures can find them in Michael Sandel's book Justice: What's the Right Thing to Do?, which is available in various languages throughout the world. This book is not required, and the course can be taken using the free online resources alone.
+
+
+
+
Do I need to watch the lectures at a specific time?
+
No. You can watch the lectures at your leisure.
+
+
+
+
Will I be able to participate in class discussions?
+
Yes, in several ways:
+
+
+
Each lecture invites you to respond to a poll question related to the themes of the lecture. If you respond to the question, you will be presented with a challenge to the opinion you have expressed, and invited to reply to the challenge. You can also, if you wish, comment on the opinions and responses posted by other students in the course, continuing the discussion.
+
+
In addition to the poll question, each class contains a discussion prompt that invites you to offer your view on a controversial question related to the lecture. If you wish, you can respond to this question, and then see what other students have to say about the argument you present. You can also comment on the opinions posted by other students. One aim of the course is to promote reasoned public dialogue about hard moral and political questions.
+
+
Each week, there will be an optional live dialogue enabling students to interact with instructors and participants from around the world.
+
+
+
+
+
Will certificates be awarded?
+
Yes. Online learners who achieve a passing grade in a course can earn a certificate of mastery. These certificates will indicate you have successfully completed the course, but will not include a specific grade. Certificates will be issued by edX under the name of HarvardX, designating the institution from which the course originated.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/prerequisites.html b/common/test/data/word_cloud/about/2013_Spring/prerequisites.html
new file mode 100644
index 0000000000..b0047fa49f
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/prerequisites.html
@@ -0,0 +1 @@
+None
diff --git a/common/test/data/word_cloud/about/2013_Spring/short_description.html b/common/test/data/word_cloud/about/2013_Spring/short_description.html
new file mode 100644
index 0000000000..208880c842
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/short_description.html
@@ -0,0 +1 @@
+JusticeX is an introduction to moral and political philosophy, including discussion of contemporary dilemmas and controversies.
\ No newline at end of file
diff --git a/common/test/data/word_cloud/about/2013_Spring/video.html b/common/test/data/word_cloud/about/2013_Spring/video.html
new file mode 100644
index 0000000000..0cf427b16c
--- /dev/null
+++ b/common/test/data/word_cloud/about/2013_Spring/video.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/common/test/data/word_cloud/chapter/Staff.xml b/common/test/data/word_cloud/chapter/Staff.xml
new file mode 100644
index 0000000000..e1d5216f6d
--- /dev/null
+++ b/common/test/data/word_cloud/chapter/Staff.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/common/test/data/word_cloud/conditional/condone.xml b/common/test/data/word_cloud/conditional/condone.xml
new file mode 100644
index 0000000000..80b061e244
--- /dev/null
+++ b/common/test/data/word_cloud/conditional/condone.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/common/test/data/word_cloud/course.xml b/common/test/data/word_cloud/course.xml
new file mode 100644
index 0000000000..1b97a5a714
--- /dev/null
+++ b/common/test/data/word_cloud/course.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/common/test/data/word_cloud/course/2013_Spring.xml b/common/test/data/word_cloud/course/2013_Spring.xml
new file mode 100644
index 0000000000..cb6e7c1217
--- /dev/null
+++ b/common/test/data/word_cloud/course/2013_Spring.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/common/test/data/word_cloud/creating_course.xml b/common/test/data/word_cloud/creating_course.xml
new file mode 100644
index 0000000000..4c90f1c2ec
--- /dev/null
+++ b/common/test/data/word_cloud/creating_course.xml
@@ -0,0 +1,8 @@
+
diff --git a/common/test/data/word_cloud/info/2013_Spring/handouts.html b/common/test/data/word_cloud/info/2013_Spring/handouts.html
new file mode 100644
index 0000000000..35f2c89474
--- /dev/null
+++ b/common/test/data/word_cloud/info/2013_Spring/handouts.html
@@ -0,0 +1,3 @@
+
+
A list of course handouts, or an empty file if there are none.