Commit 6e043d97 by Calen Pennington Committed by GitHub

Merge pull request #14296 from edx/cale/rc-merge-branch

Cale/rc merge branch
parents 35337582 2f9786ca
...@@ -278,3 +278,4 @@ Casey Litton <caseylitton@gmail.com> ...@@ -278,3 +278,4 @@ Casey Litton <caseylitton@gmail.com>
Jhony Avella <jhony.avella@edunext.co> Jhony Avella <jhony.avella@edunext.co>
Tanmay Mohapatra <tanmaykm@gmail.com> Tanmay Mohapatra <tanmaykm@gmail.com>
Brian Mesick <bmesick@edx.org> Brian Mesick <bmesick@edx.org>
Jeff LaJoie <jlajoie@edx.org>
...@@ -37,8 +37,9 @@ def get_enrollments(user_id): ...@@ -37,8 +37,9 @@ def get_enrollments(user_id):
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"user": "Bob", "user": "Bob",
"course": { "course_details": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"course_name": "edX Demonstration Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
...@@ -64,8 +65,9 @@ def get_enrollments(user_id): ...@@ -64,8 +65,9 @@ def get_enrollments(user_id):
"mode": "verified", "mode": "verified",
"is_active": True, "is_active": True,
"user": "Bob", "user": "Bob",
"course": { "course_details": {
"course_id": "edX/edX-Insider/2014T2", "course_id": "edX/edX-Insider/2014T2",
"course_name": "edX Insider Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
...@@ -111,8 +113,9 @@ def get_enrollment(user_id, course_id): ...@@ -111,8 +113,9 @@ def get_enrollment(user_id, course_id):
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"user": "Bob", "user": "Bob",
"course": { "course_details": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"course_name": "edX Demonstration Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
...@@ -163,8 +166,9 @@ def add_enrollment(user_id, course_id, mode=None, is_active=True): ...@@ -163,8 +166,9 @@ def add_enrollment(user_id, course_id, mode=None, is_active=True):
"mode": "audit", "mode": "audit",
"is_active": True, "is_active": True,
"user": "Bob", "user": "Bob",
"course": { "course_details": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"course_name": "edX Demonstration Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
...@@ -217,8 +221,9 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_ ...@@ -217,8 +221,9 @@ def update_enrollment(user_id, course_id, mode=None, is_active=None, enrollment_
"mode": "honor", "mode": "honor",
"is_active": True, "is_active": True,
"user": "Bob", "user": "Bob",
"course": { "course_details": {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"course_name": "edX Demonstration Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
...@@ -282,6 +287,7 @@ def get_course_enrollment_details(course_id, include_expired=False): ...@@ -282,6 +287,7 @@ def get_course_enrollment_details(course_id, include_expired=False):
>>> get_course_enrollment_details("edX/DemoX/2014T2") >>> get_course_enrollment_details("edX/DemoX/2014T2")
{ {
"course_id": "edX/DemoX/2014T2", "course_id": "edX/DemoX/2014T2",
"course_name": "edX Demonstration Course",
"enrollment_end": "2014-12-20T20:18:00Z", "enrollment_end": "2014-12-20T20:18:00Z",
"enrollment_start": "2014-10-15T20:18:00Z", "enrollment_start": "2014-10-15T20:18:00Z",
"course_start": "2015-02-03T00:00:00Z", "course_start": "2015-02-03T00:00:00Z",
......
...@@ -36,6 +36,7 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth ...@@ -36,6 +36,7 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
""" """
course_id = serializers.CharField(source="id") course_id = serializers.CharField(source="id")
course_name = serializers.CharField(source="display_name_with_default")
enrollment_start = serializers.DateTimeField(format=None) enrollment_start = serializers.DateTimeField(format=None)
enrollment_end = serializers.DateTimeField(format=None) enrollment_end = serializers.DateTimeField(format=None)
course_start = serializers.DateTimeField(source="start", format=None) course_start = serializers.DateTimeField(source="start", format=None)
......
...@@ -71,6 +71,7 @@ class EnrollmentDataTest(ModuleStoreTestCase): ...@@ -71,6 +71,7 @@ class EnrollmentDataTest(ModuleStoreTestCase):
# Confirm the returned enrollment and the data match up. # Confirm the returned enrollment and the data match up.
self.assertEqual(course_mode, enrollment['mode']) self.assertEqual(course_mode, enrollment['mode'])
self.assertEqual(is_active, enrollment['is_active']) self.assertEqual(is_active, enrollment['is_active'])
self.assertEqual(self.course.display_name_with_default, enrollment['course_details']['course_name'])
def test_unenroll(self): def test_unenroll(self):
# Enroll the user in the course # Enroll the user in the course
......
...@@ -191,8 +191,13 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -191,8 +191,13 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
) )
# Create an enrollment # Create an enrollment
self.assert_enrollment_status() resp = self.assert_enrollment_status()
# Verify that the response contains the correct course_name
data = json.loads(resp.content)
self.assertEqual(self.course.display_name_with_default, data['course_details']['course_name'])
# Verify that the enrollment was created correctly
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active) self.assertTrue(is_active)
...@@ -212,6 +217,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -212,6 +217,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
data = json.loads(resp.content) data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_details']['course_id']) self.assertEqual(unicode(self.course.id), data['course_details']['course_id'])
self.assertEqual(self.course.display_name_with_default, data['course_details']['course_name'])
self.assertEqual(CourseMode.DEFAULT_MODE_SLUG, data['mode']) self.assertEqual(CourseMode.DEFAULT_MODE_SLUG, data['mode'])
self.assertTrue(data['is_active']) self.assertTrue(data['is_active'])
...@@ -329,8 +335,8 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -329,8 +335,8 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
data = json.loads(response.content) data = json.loads(response.content)
self.assertItemsEqual( self.assertItemsEqual(
[enrollment['course_details']['course_id'] for enrollment in data], [(datum['course_details']['course_id'], datum['course_details']['course_name']) for datum in data],
[unicode(course.id) for course in courses] [(unicode(course.id), course.display_name_with_default) for course in courses]
) )
def test_enrollment_list_permissions(self): def test_enrollment_list_permissions(self):
...@@ -411,6 +417,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase): ...@@ -411,6 +417,7 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
data = json.loads(resp.content) data = json.loads(resp.content)
self.assertEqual(unicode(self.course.id), data['course_id']) self.assertEqual(unicode(self.course.id), data['course_id'])
self.assertEqual(self.course.display_name_with_default, data['course_name'])
mode = data['course_modes'][0] mode = data['course_modes'][0]
self.assertEqual(mode['slug'], CourseMode.HONOR) self.assertEqual(mode['slug'], CourseMode.HONOR)
self.assertEqual(mode['sku'], '123') self.assertEqual(mode['sku'], '123')
......
...@@ -99,6 +99,7 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn): ...@@ -99,6 +99,7 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn):
* course_end: The date and time when the course closes. If * course_end: The date and time when the course closes. If
null, the course never ends. null, the course never ends.
* course_id: The unique identifier for the course. * course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes * course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired include_expired=1, the array also includes expired
...@@ -216,6 +217,7 @@ class EnrollmentCourseDetailView(APIView): ...@@ -216,6 +217,7 @@ class EnrollmentCourseDetailView(APIView):
* course_end: The date and time when the course closes. If * course_end: The date and time when the course closes. If
null, the course never ends. null, the course never ends.
* course_id: The unique identifier for the course. * course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes * course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired include_expired=1, the array also includes expired
...@@ -400,6 +402,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): ...@@ -400,6 +402,8 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* course_id: The unique identifier for the course. * course_id: The unique identifier for the course.
* course_name: The name of the course.
* course_modes: An array of data about the enrollment modes * course_modes: An array of data about the enrollment modes
supported for the course. If the request uses the parameter supported for the course. If the request uses the parameter
include_expired=1, the array also includes expired include_expired=1, the array also includes expired
......
require('coffee-script');
var importAll = function(modulePath) {
module = require(modulePath);
for (key in module) {
global[key] = module[key];
}
};
importAll('mersenne-twister-min');
importAll('xproblem');
generatorModulePath = process.argv[2];
dependencies = JSON.parse(process.argv[3]);
seed = JSON.parse(process.argv[4]);
params = JSON.parse(process.argv[5]);
if (seed == null) {
seed = 4;
}
for (var i = 0; i < dependencies.length; i++) {
importAll(dependencies[i]);
}
generatorModule = require(generatorModulePath);
generatorClass = generatorModule.generatorClass;
generator = new generatorClass(seed, params);
console.log(JSON.stringify(generator.generate()));
require('coffee-script');
var importAll = function(modulePath) {
module = require(modulePath);
for (key in module) {
global[key] = module[key];
}
};
importAll('xproblem');
graderModulePath = process.argv[2];
dependencies = JSON.parse(process.argv[3]);
submission = JSON.parse(process.argv[4]);
problemState = JSON.parse(process.argv[5]);
params = JSON.parse(process.argv[6]);
for (var i = 0; i < dependencies.length; i++) {
importAll(dependencies[i]);
}
graderModule = require(graderModulePath);
graderClass = graderModule.graderClass;
grader = new graderClass(submission, problemState, params);
console.log(JSON.stringify(grader.grade()));
console.log(JSON.stringify(grader.evaluation));
console.log(JSON.stringify(grader.solution));
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
% endif % endif
/> />
<p class="status"> <p class="indicator-container">
${value|h} ${value|h}
<%include file="status_span.html" args="status=status, status_id=id"/> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
style="display:none;"/> style="display:none;"/>
<p class="status drag-and-drop--status" aria-describedby="input_${id}"> <p class="indicator-container drag-and-drop--status" aria-describedby="input_${id}">
<%include file="status_span.html" args="status=status, status_id=id"/> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input> <input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}"> <p class="indicator-container" aria-describedby="input_${id}">
<%include file="status_span.html" args="status=status, status_id=id"/> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status" aria-describedby="input_${id}"> <p class="indicator-container" aria-describedby="input_${id}">
<%include file="status_span.html" args="status=status, status_id=id"/> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
......
...@@ -43,9 +43,9 @@ ...@@ -43,9 +43,9 @@
<br/> <br/>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status"> <div class="indicator-container">
<%include file="status_span.html" args="status=status, status_id=id"/> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </div>
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
......
...@@ -389,64 +389,31 @@ div.problem { ...@@ -389,64 +389,31 @@ div.problem {
} }
} }
.unanswered {
p.status.drag-and-drop--status {
@include margin(8px, 0, 0, ($baseline/2));
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
}
}
&.correct, &.ui-icon-check { &.correct, &.ui-icon-check {
p.status {
@include status-icon($correct, $checkmark-icon);
}
input { input {
border-color: $correct; border-color: $correct;
} }
} }
&.partially-correct, &.ui-icon-check { &.partially-correct, &.ui-icon-check {
p.status {
@include status-icon($partially-correct, $asterisk-icon);
}
input { input {
border-color: $partially-correct; border-color: $partially-correct;
} }
} }
&.processing { &.processing {
p.status {
display: inline-block;
width: 20px;
height: 20px;
background: url('#{$static-path}/images/spinner.gif') center center no-repeat;
}
input { input {
border-color: #aaa; border-color: #aaa;
} }
} }
&.ui-icon-close { &.ui-icon-close {
p.status {
@include status-icon($incorrect, $cross-icon);
}
input { input {
border-color: $incorrect; border-color: $incorrect;
} }
} }
&.incorrect, &.incomplete { &.incorrect, &.incomplete {
p.status {
@include status-icon($incorrect, $cross-icon);
}
input { input {
border-color: $incorrect; border-color: $incorrect;
} }
......
--- ---
metadata: metadata:
display_name: Custom Javascript Display and Grading display_name: Custom JavaScript Display and Grading
markdown: !!null markdown: !!null
showanswer: never showanswer: never
data: | data: |
...@@ -8,8 +8,8 @@ data: | ...@@ -8,8 +8,8 @@ data: |
<p> <p>
In these problems (also called custom JavaScript problems or JS Input In these problems (also called custom JavaScript problems or JS Input
problems), you add a problem or tool that uses JavaScript in Studio. problems), you add a problem or tool that uses JavaScript in Studio.
Studio embeds the problem in an IFrame so that your students can Studio embeds the problem in an IFrame so that your learners can
interact with it in the LMS. You can grade your students' work using interact with it in the LMS. You can grade your learners' work using
JavaScript and some basic Python, and the grading is integrated into the JavaScript and some basic Python, and the grading is integrated into the
edX grading system. edX grading system.
</p> </p>
...@@ -31,42 +31,47 @@ data: | ...@@ -31,42 +31,47 @@ data: |
<p> <p>
When you add the problem, be sure to select <strong>Settings</strong> When you add the problem, be sure to select <strong>Settings</strong>
to specify a <strong>Display Name</strong> and other values that apply. to specify a <strong>Display Name</strong> and other values that apply.
Also, be sure to specify a <strong>title</strong> attribute on the <strong>jsinput</strong> tag;
this title is used for the title attribute on the generated IFrame. Generally,
the title attribute on the IFrame should match the title tag of the HTML hosted
within the IFrame, which is specified by the <strong>html_file</strong> attribute.
</p> </p>
<p>You can use the following example problem as a model.</p> <p>You can use the following example problem as a model.</p>
<customresponse cfn="vglcfn"> <customresponse cfn="check_function">
<script type="loncapa/python"> <script type="loncapa/python">
<![CDATA[ <![CDATA[
import json import json
def vglcfn(e, ans): def check_function(e, ans):
''' """
par is a dictionary that contains two keys, "answer" and "state". "response" is a dictionary that contains two keys, "answer" and "state".
The value of "answer" is the JSON string that "getGrade" returns. The value of "answer" is the JSON string that "getGrade" returns.
The value of "state" is the JSON string that "getState" returns. The value of "state" is the JSON string that "getState" returns.
Clicking either "Submit" or "Save" registers the current state. Clicking either "Submit" or "Save" registers the current state.
"""
response = json.loads(ans)
'''
par = json.loads(ans)
# You can use the value of the answer key to grade: # You can use the value of the answer key to grade:
answer = json.loads(par["answer"]) answer = json.loads(response["answer"])
return answer["cylinder"] and not answer["cube"] return answer == "correct"
'''
# Or you can use the value of the state key to grade: # Or you can use the value of the state key to grade:
state = json.loads(par["state"]) """
selectedObjects = state["selectedObjects"] state = json.loads(response["state"])
return selectedObjects["cylinder"] and not selectedObjects["cube"] return state["selectedChoice"] == "correct"
''' """
]]> ]]>
</script> </script>
<p>In the following image, click the objects until the cone is yellow and the cube is blue.</p> <p>This is paragraph text displayed before the IFrame.</p>
<jsinput gradefn="WebGLDemo.getGrade" <jsinput
get_statefn="WebGLDemo.getState" gradefn="JSInputDemo.getGrade"
set_statefn="WebGLDemo.setState" get_statefn="JSInputDemo.getState"
initial_state='{"selectedObjects":{"cube":true,"cylinder":false}}' set_statefn="JSInputDemo.setState"
width="400" initial_state='{"selectedChoice": "incorrect1", "availableChoices": ["incorrect1", "correct", "incorrect2"]}'
height="400" width="600"
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html" height="100"
title="Spinning Cone and Cube" html_file="https://files.edx.org/custom-js-example/jsinput_example.html"
title="Dropdown with Dynamic Text"
sop="false"/> sop="false"/>
</customresponse> </customresponse>
</problem> </problem>
...@@ -13,7 +13,6 @@ import textwrap ...@@ -13,7 +13,6 @@ import textwrap
import unittest import unittest
import ddt import ddt
import flaky
from lxml import etree from lxml import etree
from mock import Mock, patch, DEFAULT from mock import Mock, patch, DEFAULT
import webob import webob
...@@ -1412,7 +1411,6 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1412,7 +1411,6 @@ class CapaModuleTest(unittest.TestCase):
RANDOMIZATION.ALWAYS, RANDOMIZATION.ALWAYS,
RANDOMIZATION.ONRESET RANDOMIZATION.ONRESET
) )
@flaky.flaky # TNL-6041
def test_random_seed_with_reset(self, rerandomize): def test_random_seed_with_reset(self, rerandomize):
""" """
Run the test for each possible rerandomize value Run the test for each possible rerandomize value
...@@ -1470,13 +1468,13 @@ class CapaModuleTest(unittest.TestCase): ...@@ -1470,13 +1468,13 @@ class CapaModuleTest(unittest.TestCase):
# to another valid seed # to another valid seed
else: else:
# Since there's a small chance we might get the # Since there's a small chance (expected) we might get the
# same seed again, give it 5 chances # same seed again, give it 10 chances
# to generate a different seed # to generate a different seed
success = _retry_and_check(5, lambda: _reset_and_get_seed(module) != seed) success = _retry_and_check(10, lambda: _reset_and_get_seed(module) != seed)
self.assertIsNotNone(module.seed) self.assertIsNotNone(module.seed)
msg = 'Could not get a new seed from reset after 5 tries' msg = 'Could not get a new seed from reset after 10 tries'
self.assertTrue(success, msg) self.assertTrue(success, msg)
@ddt.data( @ddt.data(
......
.directions {
font-size: large
}
.feedback {
font-size: medium;
border: 2px solid cornflowerblue;
padding: 5px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dropdown with Dynamic Text</title>
<link rel="stylesheet" type="text/css" href="https://files.edx.org/custom-js-example/jsinput_example.css">
</head>
<body>
<script src="https://files.edx.org/custom-js-example/jschannel.js"></script>
<script src="https://files.edx.org/custom-js-example/jsinput_example.js" defer></script>
<label class="directions">Select an option from the list:
<select class="choices"></select>
</label>
<p aria-live="polite" class="feedback"></p>
</body>
</html>
/* globals Channel */
(function() {
'use strict';
// state will be populated via initial_state via the `setState` method. Defining dummy values here
// to make the expected structure clear.
var state = {
availableChoices: [],
selectedChoice: ''
},
channel,
select = document.getElementsByClassName('choices')[0],
feedback = document.getElementsByClassName('feedback')[0];
function populateSelect() {
// Populate the select from `state.availableChoices`.
var i, option;
// Clear out any pre-existing options.
while (select.firstChild) {
select.removeChild(select.firstChild);
}
// Populate the select with the available choices.
for (i = 0; i < state.availableChoices.length; i++) {
option = document.createElement('option');
option.value = i;
option.innerHTML = state.availableChoices[i];
if (state.availableChoices[i] === state.selectedChoice) {
option.selected = true;
}
select.appendChild(option);
}
feedback.innerText = "The currently selected answer is '" + state.selectedChoice + "'.";
}
function getGrade() {
// The following return value may or may not be used to grade server-side.
// If getState and setState are used, then the Python grader also gets access
// to the return value of getState and can choose it instead to grade.
return JSON.stringify(state.selectedChoice);
}
function getState() {
// Returns the current state (which can be used for grading).
return JSON.stringify(state);
}
// This function will be called with 1 argument when JSChannel is not used,
// 2 otherwise. In the latter case, the first argument is a transaction
// object that will not be used here
// (see http://mozilla.github.io/jschannel/docs/)
function setState() {
var stateString = arguments.length === 1 ? arguments[0] : arguments[1];
state = JSON.parse(stateString);
populateSelect();
}
// Establish a channel only if this application is embedded in an iframe.
// This will let the parent window communicate with this application using
// RPC and bypass SOP restrictions.
if (window.parent !== window) {
channel = Channel.build({
window: window.parent,
origin: '*',
scope: 'JSInput'
});
channel.bind('getGrade', getGrade);
channel.bind('getState', getState);
channel.bind('setState', setState);
}
select.addEventListener('change', function() {
state.selectedChoice = select.options[select.selectedIndex].text;
feedback.innerText = "You have selected '" + state.selectedChoice +
"'. Click Submit to grade your answer.";
});
return {
getState: getState,
setState: setState,
getGrade: getGrade
};
}());
...@@ -33,6 +33,7 @@ Get the User's Enrollment Status in a Course ...@@ -33,6 +33,7 @@ Get the User's Enrollment Status in a Course
"is_active": true, "is_active": true,
"course_details": { "course_details": {
"course_id": "edX/DemoX/Demo_Course", "course_id": "edX/DemoX/Demo_Course",
"course_name": "edX Demonstration Course",
"enrollment_end": null, "enrollment_end": null,
"course_modes": [ "course_modes": [
{ {
...@@ -70,6 +71,7 @@ Get the User's Enrollment Information for a Course ...@@ -70,6 +71,7 @@ Get the User's Enrollment Information for a Course
{ {
"course_id": "edX/DemoX/Demo_Course", "course_id": "edX/DemoX/Demo_Course",
"course_name": "edX Demonstration Course",
"enrollment_end": null, "enrollment_end": null,
"course_modes": [ "course_modes": [
{ {
...@@ -112,6 +114,7 @@ View a User's Enrollments or Enroll a User in a Course ...@@ -112,6 +114,7 @@ View a User's Enrollments or Enroll a User in a Course
"is_active": true, "is_active": true,
"course_details": { "course_details": {
"course_id": "edX/DemoX/Demo_Course", "course_id": "edX/DemoX/Demo_Course",
"course_name": "edX Demonstration Course",
"enrollment_end": null, "enrollment_end": null,
"course_modes": [ "course_modes": [
{ {
...@@ -135,6 +138,7 @@ View a User's Enrollments or Enroll a User in a Course ...@@ -135,6 +138,7 @@ View a User's Enrollments or Enroll a User in a Course
"is_active": true, "is_active": true,
"course_details": { "course_details": {
"course_id": "ArbisoftX/BulkyEmail101/2014-15", "course_id": "ArbisoftX/BulkyEmail101/2014-15",
"course_name": "Course Name Here",
"enrollment_end": null, "enrollment_end": null,
"course_modes": [ "course_modes": [
{ {
......
...@@ -5,6 +5,7 @@ This module contains tasks for asynchronous execution of grade updates. ...@@ -5,6 +5,7 @@ This module contains tasks for asynchronous execution of grade updates.
from celery import task from celery import task
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.utils import DatabaseError from django.db.utils import DatabaseError
from logging import getLogger from logging import getLogger
...@@ -30,6 +31,8 @@ from .transformer import GradesTransformer ...@@ -30,6 +31,8 @@ from .transformer import GradesTransformer
log = getLogger(__name__) log = getLogger(__name__)
KNOWN_RETRY_ERRORS = (DatabaseError, ValidationError) # Errors we expect occasionally, should be resolved on retry
@task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY) @task(default_retry_delay=30, routing_key=settings.RECALCULATE_GRADES_ROUTING_KEY)
def recalculate_subsection_grade( def recalculate_subsection_grade(
...@@ -72,41 +75,46 @@ def recalculate_subsection_grade_v2(**kwargs): ...@@ -72,41 +75,46 @@ def recalculate_subsection_grade_v2(**kwargs):
event_transaction_type(string): human-readable type of the event_transaction_type(string): human-readable type of the
event at the root of the current event transaction. event at the root of the current event transaction.
""" """
course_key = CourseLocator.from_string(kwargs['course_id']) try:
if not PersistentGradesEnabledFlag.feature_enabled(course_key): course_key = CourseLocator.from_string(kwargs['course_id'])
return if not PersistentGradesEnabledFlag.feature_enabled(course_key):
return
score_deleted = kwargs['score_deleted']
scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) score_deleted = kwargs['score_deleted']
expected_modified_time = from_timestamp(kwargs['expected_modified_time']) scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key)
expected_modified_time = from_timestamp(kwargs['expected_modified_time'])
# The request cache is not maintained on celery workers,
# where this code runs. So we take the values from the # The request cache is not maintained on celery workers,
# main request cache and store them in the local request # where this code runs. So we take the values from the
# cache. This correlates model-level grading events with # main request cache and store them in the local request
# higher-level ones. # cache. This correlates model-level grading events with
set_event_transaction_id(kwargs.pop('event_transaction_id', None)) # higher-level ones.
set_event_transaction_type(kwargs.pop('event_transaction_type', None)) set_event_transaction_id(kwargs.pop('event_transaction_id', None))
set_event_transaction_type(kwargs.pop('event_transaction_type', None))
# Verify the database has been updated with the scores when the task was
# created. This race condition occurs if the transaction in the task # Verify the database has been updated with the scores when the task was
# creator's process hasn't committed before the task initiates in the worker # created. This race condition occurs if the transaction in the task
# process. # creator's process hasn't committed before the task initiates in the worker
if not _has_database_updated_with_new_score( # process.
kwargs['user_id'], scored_block_usage_key, expected_modified_time, score_deleted, if not _has_database_updated_with_new_score(
): kwargs['user_id'], scored_block_usage_key, expected_modified_time, score_deleted,
raise _retry_recalculate_subsection_grade(**kwargs) ):
raise _retry_recalculate_subsection_grade(**kwargs)
_update_subsection_grades(
course_key, _update_subsection_grades(
scored_block_usage_key, course_key,
kwargs['only_if_higher'], scored_block_usage_key,
kwargs['course_id'], kwargs['only_if_higher'],
kwargs['user_id'], kwargs['user_id'],
kwargs['usage_id'], )
kwargs['expected_modified_time'],
score_deleted, except Exception as exc: # pylint: disable=broad-except
) if not isinstance(exc, KNOWN_RETRY_ERRORS):
log.info("tnl-6244 grades unexpected failure: {}. kwargs={}".format(
repr(exc),
kwargs
))
raise _retry_recalculate_subsection_grade(exc=exc, **kwargs)
def _has_database_updated_with_new_score( def _has_database_updated_with_new_score(
...@@ -138,7 +146,7 @@ def _has_database_updated_with_new_score( ...@@ -138,7 +146,7 @@ def _has_database_updated_with_new_score(
if api_score is None: if api_score is None:
# Same case as the initial 'if' above, for submissions-specific scores # Same case as the initial 'if' above, for submissions-specific scores
return score_deleted return score_deleted
reported_modified_time = api_score.created_at reported_modified_time = api_score['created_at']
else: else:
reported_modified_time = score.modified reported_modified_time = score.modified
...@@ -149,11 +157,7 @@ def _update_subsection_grades( ...@@ -149,11 +157,7 @@ def _update_subsection_grades(
course_key, course_key,
scored_block_usage_key, scored_block_usage_key,
only_if_higher, only_if_higher,
course_id,
user_id, user_id,
usage_id,
expected_modified_time,
score_deleted,
): ):
""" """
A helper function to update subsection grades in the database A helper function to update subsection grades in the database
...@@ -174,31 +178,19 @@ def _update_subsection_grades( ...@@ -174,31 +178,19 @@ def _update_subsection_grades(
course = store.get_course(course_key, depth=0) course = store.get_course(course_key, depth=0)
subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure) subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure)
try: for subsection_usage_key in subsections_to_update:
for subsection_usage_key in subsections_to_update: if subsection_usage_key in course_structure:
if subsection_usage_key in course_structure: subsection_grade = subsection_grade_factory.update(
subsection_grade = subsection_grade_factory.update( course_structure[subsection_usage_key],
course_structure[subsection_usage_key], only_if_higher,
only_if_higher, )
) SUBSECTION_SCORE_CHANGED.send(
SUBSECTION_SCORE_CHANGED.send( sender=recalculate_subsection_grade,
sender=recalculate_subsection_grade, course=course,
course=course, course_structure=course_structure,
course_structure=course_structure, user=student,
user=student, subsection_grade=subsection_grade,
subsection_grade=subsection_grade, )
)
except DatabaseError as exc:
raise _retry_recalculate_subsection_grade(
user_id,
course_id,
usage_id,
only_if_higher,
expected_modified_time,
score_deleted,
exc,
)
def _retry_recalculate_subsection_grade( def _retry_recalculate_subsection_grade(
......
...@@ -236,6 +236,18 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -236,6 +236,18 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._assert_retry_called(mock_retry) self._assert_retry_called(mock_retry)
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry') @patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
def test_retry_subsection_grade_on_update_not_complete_sub(self, mock_retry):
self.set_up_course()
with patch('lms.djangoapps.grades.tasks.sub_api.get_score') as mock_sub_score:
mock_sub_score.return_value = {
'created_at': datetime.utcnow().replace(tzinfo=pytz.UTC) - timedelta(days=1)
}
self._apply_recalculate_subsection_grade(
mock_score=MagicMock(module_type='openassessment')
)
self._assert_retry_called(mock_retry)
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
def test_retry_subsection_grade_on_no_score(self, mock_retry): def test_retry_subsection_grade_on_no_score(self, mock_retry):
self.set_up_course() self.set_up_course()
self._apply_recalculate_subsection_grade(mock_score=None) self._apply_recalculate_subsection_grade(mock_score=None)
...@@ -262,6 +274,32 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -262,6 +274,32 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade() self._apply_recalculate_subsection_grade()
self.assertEquals(mock_course_signal.call_count, 1) self.assertEquals(mock_course_signal.call_count, 1)
@patch('lms.djangoapps.grades.tasks.log')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
def test_log_unknown_error(self, mock_update, mock_retry, mock_log):
"""
Ensures that unknown errors are logged before a retry.
"""
self.set_up_course()
mock_update.side_effect = Exception("General exception with no further detail!")
self._apply_recalculate_subsection_grade()
self.assertIn("General exception with no further detail!", mock_log.info.call_args[0][0])
self._assert_retry_called(mock_retry)
@patch('lms.djangoapps.grades.tasks.log')
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
def test_no_log_known_error(self, mock_update, mock_retry, mock_log):
"""
Ensures that known errors are not logged before a retry.
"""
self.set_up_course()
mock_update.side_effect = IntegrityError("race condition oh noes")
self._apply_recalculate_subsection_grade()
self.assertFalse(mock_log.info.called)
self._assert_retry_called(mock_retry)
def _apply_recalculate_subsection_grade( def _apply_recalculate_subsection_grade(
self, self,
mock_score=MagicMock(modified=datetime.utcnow().replace(tzinfo=pytz.UTC) + timedelta(days=1)) mock_score=MagicMock(modified=datetime.utcnow().replace(tzinfo=pytz.UTC) + timedelta(days=1))
......
...@@ -263,5 +263,5 @@ class CourseTeamMembership(models.Model): ...@@ -263,5 +263,5 @@ class CourseTeamMembership(models.Model):
membership.team.save() membership.team.save()
membership.save() membership.save()
emit_team_event('edx.team.activity_updated', membership.team.course_id, { emit_team_event('edx.team.activity_updated', membership.team.course_id, {
'team_id': membership.team_id, 'team_id': membership.team.team_id,
}) })
...@@ -172,7 +172,7 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase): ...@@ -172,7 +172,7 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase):
self.assertGreater(now, team_membership.last_activity_at) self.assertGreater(now, team_membership.last_activity_at)
self.assert_event_emitted( self.assert_event_emitted(
'edx.team.activity_updated', 'edx.team.activity_updated',
team_id=team.id, team_id=team.team_id,
) )
else: else:
self.assertEqual(team.last_activity_at, team_last_activity) self.assertEqual(team.last_activity_at, team_last_activity)
......
...@@ -49,7 +49,7 @@ from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin ...@@ -49,7 +49,7 @@ from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin
PLATFORM_NAME = "Your Platform Name Here" PLATFORM_NAME = "Your Platform Name Here"
CC_MERCHANT_NAME = PLATFORM_NAME CC_MERCHANT_NAME = PLATFORM_NAME
# Shows up in the platform footer, eg "(c) COPYRIGHT_YEAR" # Shows up in the platform footer, eg "(c) COPYRIGHT_YEAR"
COPYRIGHT_YEAR = "2016" COPYRIGHT_YEAR = "2017"
PLATFORM_FACEBOOK_ACCOUNT = "http://www.facebook.com/YourPlatformFacebookAccount" PLATFORM_FACEBOOK_ACCOUNT = "http://www.facebook.com/YourPlatformFacebookAccount"
PLATFORM_TWITTER_ACCOUNT = "@YourPlatformTwitterAccount" PLATFORM_TWITTER_ACCOUNT = "@YourPlatformTwitterAccount"
......
...@@ -83,7 +83,7 @@ def wait_for_server(server, port): ...@@ -83,7 +83,7 @@ def wait_for_server(server, port):
attempts = 0 attempts = 0
server_ok = False server_ok = False
while attempts < 20: while attempts < 30:
try: try:
connection = httplib.HTTPConnection(server, port, timeout=10) connection = httplib.HTTPConnection(server, port, timeout=10)
connection.request('GET', '/') connection.request('GET', '/')
......
...@@ -53,7 +53,7 @@ git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7 ...@@ -53,7 +53,7 @@ git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6 git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev -e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip -e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
-e git+https://github.com/jazkarta/edx-jsme.git@0908b4db16168382be5685e7e9b7b4747ac410e0#egg=edx-jsme -e git+https://github.com/jazkarta/edx-jsme.git@690dbf75441fa91c7c4899df0b83d77f7deb5458#egg=edx-jsme
git+https://github.com/edx/django-pyfs.git@1.0.3#egg=django-pyfs==1.0.3 git+https://github.com/edx/django-pyfs.git@1.0.3#egg=django-pyfs==1.0.3
git+https://github.com/mitodl/django-cas.git@v2.1.1#egg=django-cas git+https://github.com/mitodl/django-cas.git@v2.1.1#egg=django-cas
-e git+https://github.com/dgrtwo/ParsePy.git@7949b9f754d1445eff8e8f20d0e967b9a6420639#egg=parse_rest -e git+https://github.com/dgrtwo/ParsePy.git@7949b9f754d1445eff8e8f20d0e967b9a6420639#egg=parse_rest
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment