From d70b74c7010f02f04377624c499b5f68543763c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 19 Nov 2012 18:18:08 +0200 Subject: [PATCH 001/736] added miller indexes grading for crystallography problems --- common/lib/capa/capa/capa_problem.py | 4 +- common/lib/capa/capa/chem/miller.py | 193 +++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 common/lib/capa/capa/chem/miller.py diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 451891d067..db42fb698a 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -33,6 +33,7 @@ from xml.sax.saxutils import unescape import chem import chem.chemcalc import chem.chemtools +import chem.miller import calc from correctmap import CorrectMap @@ -67,7 +68,8 @@ global_context = {'random': random, 'calc': calc, 'eia': eia, 'chemcalc': chem.chemcalc, - 'chemtools': chem.chemtools} + 'chemtools': chem.chemtools, + 'miller': chem.miller} # These should be removed from HTML output, including all subelements html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py new file mode 100644 index 0000000000..38b2ea88e2 --- /dev/null +++ b/common/lib/capa/capa/chem/miller.py @@ -0,0 +1,193 @@ +# 1) Calculate miller indices by points coordinates +# 2) Grader for miller indeces and lattice type + + +import numpy as np +import math +import fractions as fr +import decimal +import unittest + + +def lcm(a, b): + """ return lcm of a,b """ + return a * b / fr.gcd(a, b) + + +def section_to_fraction(distance): + """ Convert float distance, that plane cut on axis + to fraction. Return inverted fraction + + """ + if np.isnan(distance): # plane || to axis (or contains axis) + print distance, 0 + # return inverted fration to a == nan == 1/0 => 0 / 1 + return fr.Fraction(0, 1) + elif distance == 0: # plane goes through origin + return fr.Fraction(1, 1) # ERROR, need shift of coordinates + else: + # limit_denominator to closest nicest fraction + fract = fr.Fraction(distance).limit_denominator() # 5 / 2 : numerator / denominator + print 'Distance', distance, 'Inverted fraction', fract + # return inverted fraction + return fr.Fraction(fract.denominator, fract.numerator) + + +def sub_miller(sections): + ''' Calculate miller indices. + Plane does not intersect origin + ''' + fracts = [section_to_fraction(section) for section in sections] + + print sections, fracts + + common_denominator = reduce(lcm, [fract.denominator for fract in fracts]) + print 'common_denominator:', common_denominator + + # 2) lead to a common denominator + # 3) throw away denominator + miller = [fract.numerator * math.fabs(common_denominator) / fract.denominator for fract in fracts] + + # import ipdb; ipdb.set_trace() + # nice output: + output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')' + print 'Miller indices:', output + return output + + +def miller(points): + """Calculate miller indices of plane + """ + + print "\nCalculating miller indices:" + print 'Points:\n', points + # calculate normal to plane + N = np.cross(points[1] - points[0], points[2] - points[0]) + print "Normal:", N + + # origin + O = np.array([0, 0, 0]) + + # point of plane + P = points[0] + + # equation of a line for axes: O + (B-O) * t + # t - parameters, B = [Bx, By, Bz]: + B = map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]) + + # coordinates of intersections with axis: + sections = [np.dot(P - O, N) / np.dot(B_axis, N) if np.dot(B_axis, N) != 0 else np.nan for B_axis in B] + # import ipdb; ipdb.set_trace() + + if any(x == 0 for x in sections): # Plane goes through origin. + # Need to shift plane out of origin. + # For this change origin position + + # 1) find cube vertex, not crossed by plane + vertices = [ + # top + np.array([1.0, 1.0, 1.0]), + np.array([0.0, 0.0, 1.0]), + np.array([1.0, 0.0, 1.0]), + np.array([0.0, 1.0, 1.0]), + # bottom, except 0,0,0 + np.array([1.0, 0.0, 0.0]), + np.array([0.0, 1.0, 0.0]), + np.array([1.0, 1.0, 1.0]), + ] + + for vertex in vertices: + if np.dot(vertex - O, N) != 0: # vertex in plane + new_origin = vertex + break + + # get axis with center in new origin + X = np.array([1 - new_origin[0], new_origin[1], new_origin[2]]) + Y = np.array([new_origin[0], 1 - new_origin[1], new_origin[2]]) + Z = np.array([new_origin[0], new_origin[1], 1 - new_origin[2]]) + + # 2) calculate miller indexes by new origin + new_axis = [X - new_origin, Y - new_origin, Z - new_origin] + # coordinates of intersections with axis: + sections = [np.dot(P - new_origin, N) / np.dot(B_axis, N) if np.dot(B_axis, N) != 0 else np.nan for B_axis in new_axis] + # 3) fix signs of indexes + # 0 -> 1, 1 -> -1 + sections = (1 - 2 * new_origin) * sections + return sub_miller(sections) + + +class Test_Crystallography_Grader(unittest.TestCase): + ''' test crystallography grade function ''' + + def test_1(self): + x = np.array([0.5, 0, 0]) + y = np.array([0, 0.5, 0]) + z = np.array([0, 0, 0.5]) + self.assertEqual(miller(np.array([x, y, z])), '(222)') + + def test_2(self): + x = np.array([1, 0, 0]) + y = np.array([0, 1, 0]) + z = np.array([0, 0, 1]) + self.assertEqual(miller(np.array([x, y, z])), '(111)') + + def test_3(self): + x = np.array([1, 0.5, 1]) + y = np.array([1, 1, 0.5]) + z = np.array([0.5, 1, 1]) + self.assertEqual(miller(np.array([x, y, z])), '(222)') + + def test_4(self): + x = np.array([1. / 3, 1., 0]) + y = np.array([0, 2. / 3., 0]) + z = np.array([0, 1, 1. / 3]) + self.assertEqual(miller(np.array([x, y, z])), '(-33-3)') + + def test_5(self): + x = np.array([1. / 3, 1., 0]) + y = np.array([0, 1. / 3., 0]) + z = np.array([0, 1, 1. / 3]) + self.assertEqual(miller(np.array([x, y, z])), '(-63-6)') + + def test_6(self): + x = np.array([0, 1. / 4., 0]) + y = np.array([1. / 4, 0, 0]) + z = np.array([0, 0, 1. / 4]) + self.assertEqual(miller(np.array([x, y, z])), '(444)') + + def test_7(self): # goes throug origin + x = np.array([0, 1., 0]) + y = np.array([1., 0, 0]) + z = np.array([0.5, 1., 0]) + self.assertEqual(miller(np.array([x, y, z])), '(00-1)') + + def test_8(self): + x = np.array([0, 1., 0.5]) + y = np.array([1., 0, 0.5]) + z = np.array([0.5, 1., 0.5]) + self.assertEqual(miller(np.array([x, y, z])), '(002)') + + def test_9(self): + x = np.array([0, 1. / 4., 0]) + y = np.array([1. / 4, 0, 0]) + z = np.array([0, 0, 1. / 4]) + self.assertEqual(miller(np.array([x, y, z])), '(444)') + + def test_10(self): + x = np.array([0, 1. / 4., 0]) + y = np.array([1. / 4, 0, 0]) + z = np.array([0, 0, 1. / 4]) + self.assertEqual(miller(np.array([x, y, z])), '(444)') + + +def suite(): + + testcases = [Test_Crystallography_Grader] + suites = [] + for testcase in testcases: + suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) + return unittest.TestSuite(suites) + +if __name__ == "__main__": + unittest.TextTestRunner(verbosity=2).run(suite()) + From ae37b621a8634aff8a4ec8fb2b5456a3609d8e85 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 19 Nov 2012 18:19:36 +0200 Subject: [PATCH 002/736] Update to crystallography html. --- common/lib/capa/capa/templates/crystallography.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 2370f59dd2..5be1030eb0 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -2,7 +2,6 @@
-
@@ -19,7 +18,7 @@
% endif - Date: Tue, 20 Nov 2012 14:19:48 +0200 Subject: [PATCH 003/736] Updated crystallography template to reflect changes made in the JS part. --- .../capa/capa/templates/crystallography.html | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 5be1030eb0..3099faef7b 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -1,33 +1,37 @@
-
+
+ + +
- % if status == 'unsubmitted': -
+
% elif status == 'correct': -
+
% elif status == 'incorrect': -
+
% elif status == 'incomplete': -
+
% endif + % if hidden: -
+
% endif - + + % if hidden: + style="display:none;" + % endif + /> + +

% if status == 'unsubmitted': unanswered % elif status == 'correct': @@ -37,14 +41,15 @@ % elif status == 'incomplete': incomplete % endif -

+

-

+

- % if msg: - ${msg|n} - % endif -% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: -
-% endif + % if msg: + ${msg|n} + % endif + + % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +
+ % endif
From aaeffa5316f526e34b6c12db51a1952fcda81d4d Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Wed, 21 Nov 2012 16:52:17 +0200 Subject: [PATCH 004/736] Updated template for crystallography so that it corresponds to the cnahges made in the JS part. --- common/lib/capa/capa/templates/crystallography.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 3099faef7b..30d9693ecb 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -1,7 +1,13 @@
- +
+ Type: +
+
+ Triangle: + Full: +
From d5dd725537786dc4b739301197ced40defb5569c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Wed, 21 Nov 2012 17:53:51 +0200 Subject: [PATCH 005/736] miller indices calculation --- common/lib/capa/capa/chem/miller.py | 176 ++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 52 deletions(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index 38b2ea88e2..a3adc7ffb4 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -1,12 +1,9 @@ -# 1) Calculate miller indices by points coordinates -# 2) Grader for miller indeces and lattice type - - import numpy as np import math import fractions as fr import decimal import unittest +import json def lcm(a, b): @@ -27,7 +24,8 @@ def section_to_fraction(distance): return fr.Fraction(1, 1) # ERROR, need shift of coordinates else: # limit_denominator to closest nicest fraction - fract = fr.Fraction(distance).limit_denominator() # 5 / 2 : numerator / denominator + # import ipdb; ipdb.set_trace() + fract = fr.Fraction(distance).limit_denominator(10) # 5 / 2 : numerator / denominator print 'Distance', distance, 'Inverted fraction', fract # return inverted fraction return fr.Fraction(fract.denominator, fract.numerator) @@ -50,7 +48,9 @@ def sub_miller(sections): # import ipdb; ipdb.set_trace() # nice output: - output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')' + # output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')' + # import ipdb; ipdb.set_trace() + output = '(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')' print 'Miller indices:', output return output @@ -59,11 +59,11 @@ def miller(points): """Calculate miller indices of plane """ - print "\nCalculating miller indices:" - print 'Points:\n', points + # print "\nCalculating miller indices:" + # print 'Points:\n', points # calculate normal to plane N = np.cross(points[1] - points[0], points[2] - points[0]) - print "Normal:", N + # print "Normal:", N # origin O = np.array([0, 0, 0]) @@ -116,73 +116,146 @@ def miller(points): return sub_miller(sections) -class Test_Crystallography_Grader(unittest.TestCase): +def grade(user_input, correct_answer): + ''' + Format: + user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"],["0.78","1.00","0.00"],["0.00","1.00","0.72"]]} + "lattice" is one of: "", "sc", "bcc", "fcc" + correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'} + ''' + def negative(s): + # import ipdb; ipdb.set_trace() + output = '' + i = 1 + while i in range(1, len(s) - 1): + if s[i] in (',', ' '): + output += s[i] + elif s[i] not in ('-', '0'): + output += '-' + s[i] + elif s[i] == '0': + output += s[i] + else: + i += 1 + output += s[i] + i += 1 + # import ipdb; ipdb.set_trace() + return '(' + output + ')' + + user_answer = json.loads(user_input) + + if user_answer['lattice'] != correct_answer['lattice']: + return False + + points = [map(float, p) for p in user_answer['points']] + points = [np.array(point) for point in points] + # import ipdb; ipdb.set_trace() + print miller(points), (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')) + + if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')): + return True + + return False + + +class Test_Crystallography_Miller(unittest.TestCase): ''' test crystallography grade function ''' def test_1(self): - x = np.array([0.5, 0, 0]) - y = np.array([0, 0.5, 0]) - z = np.array([0, 0, 0.5]) - self.assertEqual(miller(np.array([x, y, z])), '(222)') + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' + self.assertTrue(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) def test_2(self): - x = np.array([1, 0, 0]) - y = np.array([0, 1, 0]) - z = np.array([0, 0, 1]) - self.assertEqual(miller(np.array([x, y, z])), '(111)') + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'})) def test_3(self): - x = np.array([1, 0.5, 1]) - y = np.array([1, 1, 0.5]) - z = np.array([0.5, 1, 1]) - self.assertEqual(miller(np.array([x, y, z])), '(222)') + user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) def test_4(self): - x = np.array([1. / 3, 1., 0]) - y = np.array([0, 2. / 3., 0]) - z = np.array([0, 1, 1. / 3]) - self.assertEqual(miller(np.array([x, y, z])), '(-33-3)') + user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}' + self.assertTrue(grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'})) def test_5(self): - x = np.array([1. / 3, 1., 0]) - y = np.array([0, 1. / 3., 0]) - z = np.array([0, 1, 1. / 3]) - self.assertEqual(miller(np.array([x, y, z])), '(-63-6)') + user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}' + self.assertTrue(grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) def test_6(self): - x = np.array([0, 1. / 4., 0]) - y = np.array([1. / 4, 0, 0]) - z = np.array([0, 0, 1. / 4]) - self.assertEqual(miller(np.array([x, y, z])), '(444)') + user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}' + self.assertTrue(grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'})) def test_7(self): # goes throug origin - x = np.array([0, 1., 0]) - y = np.array([1., 0, 0]) - z = np.array([0.5, 1., 0]) - self.assertEqual(miller(np.array([x, y, z])), '(00-1)') + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'})) def test_8(self): - x = np.array([0, 1., 0.5]) - y = np.array([1., 0, 0.5]) - z = np.array([0.5, 1., 0.5]) - self.assertEqual(miller(np.array([x, y, z])), '(002)') + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'})) def test_9(self): - x = np.array([0, 1. / 4., 0]) - y = np.array([1. / 4, 0, 0]) - z = np.array([0, 0, 1. / 4]) - self.assertEqual(miller(np.array([x, y, z])), '(444)') + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'})) def test_10(self): - x = np.array([0, 1. / 4., 0]) - y = np.array([1. / 4, 0, 0]) - z = np.array([0, 0, 1. / 4]) - self.assertEqual(miller(np.array([x, y, z])), '(444)') + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) + + def test_11(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'})) + + def test_12(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'})) + + def test_13(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'})) + + def test_14(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'})) + + def test_15(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) + + def test_16(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) + + def test_17(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'})) + + def test_18(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) + + def test_19(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'})) + + def test_20(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'})) + + def test_21(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'})) + + def test_22(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'})) + + def test_23(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) def suite(): - testcases = [Test_Crystallography_Grader] + testcases = [Test_Crystallography_Miller] suites = [] for testcase in testcases: suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) @@ -190,4 +263,3 @@ def suite(): if __name__ == "__main__": unittest.TextTestRunner(verbosity=2).run(suite()) - From 33159824a5593d3e30422924ec31221a9e16c333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Wed, 21 Nov 2012 18:02:48 +0200 Subject: [PATCH 006/736] added test for lattices --- common/lib/capa/capa/chem/miller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index a3adc7ffb4..3fa49c046b 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -252,6 +252,10 @@ class Test_Crystallography_Miller(unittest.TestCase): user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' self.assertTrue(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) + def test_wrong_lattice(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertFalse(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'fcc'})) + def suite(): From 111667defdb29b1d09004a93357ded0a890262bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Wed, 21 Nov 2012 19:02:40 +0200 Subject: [PATCH 007/736] UI delta and tests: --- common/lib/capa/capa/chem/miller.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index 3fa49c046b..8a98732874 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -16,12 +16,13 @@ def section_to_fraction(distance): to fraction. Return inverted fraction """ + # import ipdb; ipdb.set_trace() if np.isnan(distance): # plane || to axis (or contains axis) print distance, 0 # return inverted fration to a == nan == 1/0 => 0 / 1 return fr.Fraction(0, 1) - elif distance == 0: # plane goes through origin - return fr.Fraction(1, 1) # ERROR, need shift of coordinates + elif math.fabs(distance) <= 0.05: # plane goes through origin, 0.02 - UI delta + return fr.Fraction(1 if distance >= 0 else -1, 1) # ERROR, need shift of coordinates else: # limit_denominator to closest nicest fraction # import ipdb; ipdb.set_trace() @@ -51,7 +52,7 @@ def sub_miller(sections): # output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')' # import ipdb; ipdb.set_trace() output = '(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')' - print 'Miller indices:', output + # print 'Miller indices:', output return output @@ -252,9 +253,17 @@ class Test_Crystallography_Miller(unittest.TestCase): user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' self.assertTrue(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) + def test_24(self): + user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}' + self.assertTrue(grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'})) + + def test_25(self): + user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''})) + def test_wrong_lattice(self): user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertFalse(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'fcc'})) + self.assertFalse(grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) def suite(): From 166ff823a79e52eab0e33e72aa68dbb563b548ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Thu, 22 Nov 2012 12:53:21 +0200 Subject: [PATCH 008/736] removed hidden parameter to template, style='hidden' is set by default for input --- common/lib/capa/capa/inputtypes.py | 7 ++----- common/lib/capa/capa/templates/crystallography.html | 13 +------------ 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 0b2250f98d..ec1cda83c7 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -671,18 +671,15 @@ class Crystallography(InputTypeBase): """ Note: height, width are required. """ - return [Attribute('size', None), - Attribute('height'), + return [Attribute('height'), Attribute('width'), - - # can probably be removed (textline should prob be always-hidden) - Attribute('hidden', ''), ] registry.register(Crystallography) # ------------------------------------------------------------------------- + class VseprInput(InputTypeBase): """ Input for molecular geometry--show possible structures, let student diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 30d9693ecb..79779384c8 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -23,19 +23,8 @@
% endif - % if hidden: -
- % endif - +

% if status == 'unsubmitted': From 19c463b7390a5bde585669ff9246e3a690c2d00e Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 22 Nov 2012 13:55:50 +0200 Subject: [PATCH 009/736] Removed radio buttons for selecting the intersection type from crystallography template. --- common/lib/capa/capa/templates/crystallography.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 30d9693ecb..1d679ff445 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -4,10 +4,6 @@

Type:
-
- Triangle: - Full: -
From 3e8e576e28b1caf26808e9f9b99a5bbb66719c63 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 22 Nov 2012 16:52:59 +0200 Subject: [PATCH 010/736] Slight change in caption for lattice select. --- common/lib/capa/capa/templates/crystallography.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 102b6136bb..8dcbff354b 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -2,7 +2,7 @@
- Type: + Lattice:
From 77c831eb103878e22765eb183e359111f393d893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Thu, 22 Nov 2012 18:43:41 +0200 Subject: [PATCH 011/736] documenation for chem.miller --- docs/source/capa.rst | 6 ++++ docs/source/chem.rst | 69 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 docs/source/chem.rst diff --git a/docs/source/capa.rst b/docs/source/capa.rst index f83d89f52d..345855af5e 100644 --- a/docs/source/capa.rst +++ b/docs/source/capa.rst @@ -1,9 +1,15 @@ ******************************************* Capa module ******************************************* +Contents: .. module:: capa +.. toctree:: + :maxdepth: 2 + + chem.rst + Calc ==== diff --git a/docs/source/chem.rst b/docs/source/chem.rst new file mode 100644 index 0000000000..26e01a3238 --- /dev/null +++ b/docs/source/chem.rst @@ -0,0 +1,69 @@ +******************************************* +Chem module +******************************************* + +.. module:: chem + +Miller +====== + +.. automodule:: capa.chem.miller + :members: + :show-inheritance: + +UI part and inputtypes +---------------------- +Miller module is used in the system in crystallography problems. +Crystallography is a class in :mod:`capa` inputtypes module. +It uses *crystallography.html* for rendering and **crystallography.js** +for UI part. + +Documentation from **crystallography.js**:: + + For a crystallographic problem of the type + + Given a plane definition via miller indexes, specify it by plotting points on the edges + of a 3D cube. Additionally, select the correct Bravais cubic lattice type depending on the + physical crystal mentioned in the problem. + + we create a graph which contains a cube, and a 3D Cartesian coordinate system. The interface + will allow to plot 3 points anywhere along the edges of the cube, and select which type of + Bravais lattice should be displayed along with the basic cube outline. + + When 3 points are successfully plotted, an intersection of the resulting plane (defined by + the 3 plotted points), and the cube, will be automatically displayed for clarity. + + After lotting the three points, it is possible to continue plotting additional points. By + doing so, the point that was plotted first (from the three that already exist), will be + removed, and the new point will be added. The intersection of the resulting new plane and + the cube will be redrawn. + + The UI has been designed in such a way, that the user is able to determine which point will + be removed next (if adding a new point). This is achieved via filling the to-be-removed point + with a different color. + + + +Chemcalc +======== + +.. automodule:: capa.chem.chemcalc + :members: + :show-inheritance: + +Chemtools +========= + +.. automodule:: capa.chem.chemtools + :members: + :show-inheritance: + + +Tests +===== + +.. automodule:: capa.chem.tests + :members: + :show-inheritance: + + From 4b4b2c9927dd28de403cd5488b29ec280465f1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Thu, 22 Nov 2012 18:44:31 +0200 Subject: [PATCH 012/736] added documentation, tests, and rounding to closes 0.05 value --- common/lib/capa/capa/chem/miller.py | 322 +++++++++++++++++++--------- 1 file changed, 223 insertions(+), 99 deletions(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index 8a98732874..f397e9d35e 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -1,3 +1,5 @@ +""" Calculation of Miller indices """ + import numpy as np import math import fractions as fr @@ -7,150 +9,256 @@ import json def lcm(a, b): - """ return lcm of a,b """ + """ + Returns least common multiple of a, b + + Args: + a, b: floats + + Returns: + float + """ return a * b / fr.gcd(a, b) -def section_to_fraction(distance): - """ Convert float distance, that plane cut on axis - to fraction. Return inverted fraction +def segment_to_fraction(distance): + """ + Converts lengths of which the plane cuts the axes to fraction. + + Tries convert distance to closest nicest fraction with denominator less or + equal than 10. It is + purely for simplicity and clearance of learning purposes. Jenny: 'In typical + courses students usually do not encounter indices any higher than 6'. + + If distance is not a number (numpy nan), it means that plane is parallel to + axis or contains it. Inverted fraction to nan (nan is 1/0) = 0 / 1 is + returned + + Generally (special cases): + + a) if distance is smaller than some constant, i.g. 0.01011, + than fraction's denominator usually much greater than 10. + + b) Also, if student will set point on 0.66 -> 1/3, so it is 333 plane, + But if he will slightly move the mouse and click on 0.65 -> it will be + (16,15,16) plane. That's why we are doing adjustments for points coordinates, + to the closest tick, tick + tick / 2 value. And now UI sends to server only + values multiple to 0.05 (half of tick). Same rounding is implemented for + unittests. + + But if one will want to calculate miller indices with exact coordinates and + with nice fractions (which produce small Miller indices), he may want shift + to new origin if segments are like S = (0.015, > 0.05, >0.05) - close to zero + in one coordinate. He may update S to (0, >0.05, >0.05) and shift origin. + In this way he can recieve nice small fractions. Also there is can be + degenerated case when S = (0.015, 0.012, >0.05) - if update S to (0, 0, >0.05) - + it is a line. This case should be considered separately. Small nice Miller + numbers and possibility to create very small segments can not be implemented + at same time). + + + Args: + distance: float distance that plane cuts on axis, it must not be 0. + Distance is multiple of 0.05. + + Returns: + Inverted fraction. + 0 / 1 if distance is nan """ - # import ipdb; ipdb.set_trace() - if np.isnan(distance): # plane || to axis (or contains axis) - print distance, 0 - # return inverted fration to a == nan == 1/0 => 0 / 1 + if np.isnan(distance): return fr.Fraction(0, 1) - elif math.fabs(distance) <= 0.05: # plane goes through origin, 0.02 - UI delta - return fr.Fraction(1 if distance >= 0 else -1, 1) # ERROR, need shift of coordinates else: - # limit_denominator to closest nicest fraction - # import ipdb; ipdb.set_trace() - fract = fr.Fraction(distance).limit_denominator(10) # 5 / 2 : numerator / denominator - print 'Distance', distance, 'Inverted fraction', fract - # return inverted fraction + fract = fr.Fraction(distance).limit_denominator(10) return fr.Fraction(fract.denominator, fract.numerator) -def sub_miller(sections): - ''' Calculate miller indices. - Plane does not intersect origin +def sub_miller(segments): ''' - fracts = [section_to_fraction(section) for section in sections] + Calculates Miller indices from segments. - print sections, fracts + Algorithm: + 1. Obtain inverted fraction from segments + + 2. Find common denominator of inverted fractions + + 3. Lead fractions to common denominator and throws denominator away. + + 4. Return obtained values. + + Args: + List of 3 floats, meaning distances that plane cuts on x, y, z axes. + Any float not equals zero, it means that plane does not intersect origin, + i. e. shift of origin has already been done. + + Returns: + String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2) + ''' + fracts = [segment_to_fraction(segment) for segment in segments] common_denominator = reduce(lcm, [fract.denominator for fract in fracts]) - print 'common_denominator:', common_denominator - - # 2) lead to a common denominator - # 3) throw away denominator - miller = [fract.numerator * math.fabs(common_denominator) / fract.denominator for fract in fracts] - - # import ipdb; ipdb.set_trace() - # nice output: - # output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')' - # import ipdb; ipdb.set_trace() - output = '(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')' - # print 'Miller indices:', output - return output + miller = ([fract.numerator * math.fabs(common_denominator) / + fract.denominator for fract in fracts]) + return'(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')' def miller(points): - """Calculate miller indices of plane + """ + Calculates Miller indices from points. + + Algorithm: + + 1. Calculate normal vector to a plane that goes trough all points. + + 2. Set origin. + + 3. Create Cartesian coordinate system (Ccs). + + 4. Find the lengths of segments of which the plane cuts the axes. Equation + of a line for axes: Origin + (Coordinate_vector - Origin) * parameter. + + 5. If plane goes trough Origin: + + a) Find new random origin: find unit cube vertex, not crossed by a plane. + + b) Repeat 2-4. + + c) Fix signs of segments after Origin shift. This means to consider + original directions of axes. I.g.: Origin was 0,0,0 and became + new_origin. If new_origin has same Y coordinate as Origin, then segment + does not change its sign. But if new_origin has another Y coordinate than + origin (was 0, became 1), than segment has to change its sign (it now + lies on negative side of Y axis). New Origin 0 value of X or Y or Z + coordinate means that segment does not change sign, 1 value -> does + change. So new sign is (1 - 2 * new_origin): 0 -> 1, 1 -> -1 + + 6. Run function that calculates miller indices from segments. + + Args: + List of points. Each point is list of float coordinates. Order of + coordinates in point's list: x, y, z. Points are different! + + Returns: + String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2) """ - # print "\nCalculating miller indices:" - # print 'Points:\n', points - # calculate normal to plane N = np.cross(points[1] - points[0], points[2] - points[0]) - # print "Normal:", N - - # origin O = np.array([0, 0, 0]) - - # point of plane - P = points[0] - - # equation of a line for axes: O + (B-O) * t - # t - parameters, B = [Bx, By, Bz]: - B = map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]) - - # coordinates of intersections with axis: - sections = [np.dot(P - O, N) / np.dot(B_axis, N) if np.dot(B_axis, N) != 0 else np.nan for B_axis in B] - # import ipdb; ipdb.set_trace() - - if any(x == 0 for x in sections): # Plane goes through origin. - # Need to shift plane out of origin. - # For this change origin position - - # 1) find cube vertex, not crossed by plane - vertices = [ - # top - np.array([1.0, 1.0, 1.0]), - np.array([0.0, 0.0, 1.0]), - np.array([1.0, 0.0, 1.0]), - np.array([0.0, 1.0, 1.0]), - # bottom, except 0,0,0 - np.array([1.0, 0.0, 0.0]), - np.array([0.0, 1.0, 0.0]), - np.array([1.0, 1.0, 1.0]), - ] - + P = points[0] # point of plane + Ccs = map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]) + segments = ([np.dot(P - O, N) / np.dot(ort, N) if np.dot(ort, N) != 0 else + np.nan for ort in Ccs]) + if any(x == 0 for x in segments): # Plane goes through origin. + vertices = [ # top: + np.array([1.0, 1.0, 1.0]), + np.array([0.0, 0.0, 1.0]), + np.array([1.0, 0.0, 1.0]), + np.array([0.0, 1.0, 1.0]), + # bottom, except 0,0,0: + np.array([1.0, 0.0, 0.0]), + np.array([0.0, 1.0, 0.0]), + np.array([1.0, 1.0, 1.0]), + ] for vertex in vertices: - if np.dot(vertex - O, N) != 0: # vertex in plane + if np.dot(vertex - O, N) != 0: # vertex not in plane new_origin = vertex break - - # get axis with center in new origin + # obtain new axes with center in new origin X = np.array([1 - new_origin[0], new_origin[1], new_origin[2]]) Y = np.array([new_origin[0], 1 - new_origin[1], new_origin[2]]) Z = np.array([new_origin[0], new_origin[1], 1 - new_origin[2]]) + new_Ccs = [X - new_origin, Y - new_origin, Z - new_origin] + segments = ([np.dot(P - new_origin, N) / np.dot(ort, N) if + np.dot(ort, N) != 0 else np.nan for ort in new_Ccs]) + # fix signs of indices: 0 -> 1, 1 -> -1 ( + segments = (1 - 2 * new_origin) * segments - # 2) calculate miller indexes by new origin - new_axis = [X - new_origin, Y - new_origin, Z - new_origin] - # coordinates of intersections with axis: - sections = [np.dot(P - new_origin, N) / np.dot(B_axis, N) if np.dot(B_axis, N) != 0 else np.nan for B_axis in new_axis] - # 3) fix signs of indexes - # 0 -> 1, 1 -> -1 - sections = (1 - 2 * new_origin) * sections - return sub_miller(sections) + return sub_miller(segments) def grade(user_input, correct_answer): ''' - Format: - user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"],["0.78","1.00","0.00"],["0.00","1.00","0.72"]]} - "lattice" is one of: "", "sc", "bcc", "fcc" - correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'} + Grade crystallography problem. + + Returns true if lattices are the same and Miller indices are same or minus + same. E.g. (2,2,2) = (2, 2, 2) or (-2, -2, -2). Because sign depends only + on student's selection of origin. + + Args: + user_input, correct_answer: json. Format: + + user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"], + ["0.78","1.00","0.00"],["0.00","1.00","0.72"]]} + + correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'} + + "lattice" is one of: "", "sc", "bcc", "fcc" + + Returns: + True or false. ''' - def negative(s): - # import ipdb; ipdb.set_trace() + def negative(m): + """ + Change sign of Miller indices. + + Args: + m: string with meaning of Miller indices. E.g.: + (-6,3,-6) -> (6, -3, 6) + + Returns: + String with changed signs. + """ output = '' i = 1 - while i in range(1, len(s) - 1): - if s[i] in (',', ' '): - output += s[i] - elif s[i] not in ('-', '0'): - output += '-' + s[i] - elif s[i] == '0': - output += s[i] + while i in range(1, len(m) - 1): + if m[i] in (',', ' '): + output += m[i] + elif m[i] not in ('-', '0'): + output += '-' + m[i] + elif m[i] == '0': + output += m[i] else: i += 1 - output += s[i] + output += m[i] i += 1 - # import ipdb; ipdb.set_trace() return '(' + output + ')' + def round0_25(point): + """ + Rounds point coordinates to closest 0.5 value. + + Args: + point: list of float coordinates. Order of coordinates: x, y, z. + + Returns: + list of coordinates rounded to closes 0.5 value + """ + rounded_points = [] + for coord in point: + base = math.floor(coord * 10) + fractional_part = (coord * 10 - base) + aliquot0_25 = math.floor(fractional_part / 0.25) + if aliquot0_25 == 0.0: + rounded_points.append(base / 10) + if aliquot0_25 in (1.0, 2.0): + rounded_points.append(base / 10 + 0.05) + if aliquot0_25 == 3.0: + rounded_points.append(base / 10 + 0.1) + return rounded_points + user_answer = json.loads(user_input) if user_answer['lattice'] != correct_answer['lattice']: return False points = [map(float, p) for p in user_answer['points']] + + # round point to closes 0.05 value + points = [round0_25(point) for point in points] + points = [np.array(point) for point in points] - # import ipdb; ipdb.set_trace() - print miller(points), (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')) + print miller(points), (correct_answer['miller'].replace(' ', ''), + negative(correct_answer['miller']).replace(' ', '')) if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')): return True @@ -159,7 +267,7 @@ def grade(user_input, correct_answer): class Test_Crystallography_Miller(unittest.TestCase): - ''' test crystallography grade function ''' + ''' Tests for crystallography grade function.''' def test_1(self): user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' @@ -178,8 +286,10 @@ class Test_Crystallography_Miller(unittest.TestCase): self.assertTrue(grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'})) def test_5(self): + """ return true only in case points coordinates are exact. + But if they transform to closest 0.05 value it is not true""" user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}' - self.assertTrue(grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) + self.assertFalse(grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) def test_6(self): user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}' @@ -261,6 +371,20 @@ class Test_Crystallography_Miller(unittest.TestCase): user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}' self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''})) + def test_26(self): + user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}' + self.assertTrue(grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''})) + + def test_27(self): + """ rounding to 0.35""" + user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}' + self.assertTrue(grade(user_input, {'miller': '(3,3,3)', 'lattice': ''})) + + def test_28(self): + """ rounding to 0.30""" + user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}' + self.assertTrue(grade(user_input, {'miller': '(10,10,10)', 'lattice': ''})) + def test_wrong_lattice(self): user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' self.assertFalse(grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) From 01b59923726d6a42841adabc23c41fcea9e7905e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Thu, 22 Nov 2012 18:53:35 +0200 Subject: [PATCH 013/736] comment outputs --- common/lib/capa/capa/chem/miller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index f397e9d35e..f1482de80f 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -257,8 +257,8 @@ def grade(user_input, correct_answer): points = [round0_25(point) for point in points] points = [np.array(point) for point in points] - print miller(points), (correct_answer['miller'].replace(' ', ''), - negative(correct_answer['miller']).replace(' ', '')) + # print miller(points), (correct_answer['miller'].replace(' ', ''), + # negative(correct_answer['miller']).replace(' ', '')) if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')): return True From 7da7bf748155a52578288eb8967584d12d78a339 Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Mon, 26 Nov 2012 14:16:01 -0500 Subject: [PATCH 014/736] Add pip install of test requirements, remove askbot requirements. --- jenkins/test_edge.sh | 2 +- jenkins/test_lms.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/test_edge.sh b/jenkins/test_edge.sh index 7b58b481f6..ccca91fc37 100755 --- a/jenkins/test_edge.sh +++ b/jenkins/test_edge.sh @@ -12,8 +12,8 @@ export PYTHONIOENCODING=UTF-8 GIT_BRANCH=${GIT_BRANCH/HEAD/master} pip install -q -r pre-requirements.txt +pip install -q -r test-requirements.txt yes w | pip install -q -r requirements.txt -[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt rake clobber TESTS_FAILED=0 diff --git a/jenkins/test_lms.sh b/jenkins/test_lms.sh index 98640c2b5b..a7f8555f81 100755 --- a/jenkins/test_lms.sh +++ b/jenkins/test_lms.sh @@ -12,8 +12,8 @@ export PYTHONIOENCODING=UTF-8 GIT_BRANCH=${GIT_BRANCH/HEAD/master} pip install -q -r pre-requirements.txt +pip install -q -r test-requirements.txt yes w | pip install -q -r requirements.txt -[ ! -d askbot ] || pip install -q -r askbot/askbot_requirements.txt rake clobber TESTS_FAILED=0 From 474f810b3843432b1a41541d7d7f2e214b102faa Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 16:29:45 -0500 Subject: [PATCH 015/736] Adding lowest_passing_grade() to CourseDescriptor --- common/lib/xmodule/xmodule/course_module.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 2b6232d366..3506c72bd7 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -149,6 +149,10 @@ class CourseDescriptor(SequenceDescriptor): def grade_cutoffs(self): return self._grading_policy['GRADE_CUTOFFS'] + @property + def lowest_passing_grade(self): + return min(self._grading_policy['GRADE_CUTOFFS'].values()) + @property def tabs(self): """ @@ -292,7 +296,7 @@ class CourseDescriptor(SequenceDescriptor): return False except: log.exception("Error parsing discussion_blackouts for course {0}".format(self.id)) - + return True @property From 631580ccaf9f4701c84c811b2e7a61adba1cfacf Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 16:30:23 -0500 Subject: [PATCH 016/736] Changing grade info on the dashboard --- lms/templates/dashboard.html | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 177eb276af..a367c36b38 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -243,16 +243,13 @@ %>
- % if cert_status['status'] == 'processing': + % if cert_status['status'] in ('processing', 'generating'):

Final course details are being wrapped up at this time. Your final standing will be available shortly.

- % elif cert_status['status'] in ('generating', 'ready'): -

You have received a grade of - ${cert_status['grade']} - in this course.

- % elif cert_status['status'] == 'notpassing': -

You did not complete the necessary requirements for - completion of this course.

+ % elif cert_status['status'] in ('generating', 'ready', 'notpassing'): +

Your final grade: + ${cert_status['grade']}, + grade required for certificate: ${course.lowest_passing_grade()}.

% endif % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: From 9148d49ac72f98e929bf2045fc9db377105ba058 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 16:57:00 -0500 Subject: [PATCH 017/736] lowest_passing_grade fix --- lms/templates/dashboard.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index a367c36b38..c293af0c29 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -249,7 +249,7 @@ % elif cert_status['status'] in ('generating', 'ready', 'notpassing'):

Your final grade: ${cert_status['grade']}, - grade required for certificate: ${course.lowest_passing_grade()}.

+ grade required for certificate: ${course.lowest_passing_grade}.

% endif % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: From 4e4a29e030b42e5e575902e2b8b597cc5c49007d Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 17:40:55 -0500 Subject: [PATCH 018/736] Removing unnecessary test for 'generating' --- lms/templates/dashboard.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index c293af0c29..aa4dac98a0 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -246,7 +246,7 @@ % if cert_status['status'] in ('processing', 'generating'):

Final course details are being wrapped up at this time. Your final standing will be available shortly.

- % elif cert_status['status'] in ('generating', 'ready', 'notpassing'): + % elif cert_status['status'] in ('ready', 'notpassing'):

Your final grade: ${cert_status['grade']}, grade required for certificate: ${course.lowest_passing_grade}.

From d13509e6f303afd658721c94cea317b3d7f4eaa3 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 17:42:55 -0500 Subject: [PATCH 019/736] Adding percentage formatting for final grade reporting --- lms/templates/dashboard.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index aa4dac98a0..cbba8bd4b5 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -248,8 +248,9 @@ this time. Your final standing will be available shortly.

% elif cert_status['status'] in ('ready', 'notpassing'):

Your final grade: - ${cert_status['grade']}, - grade required for certificate: ${course.lowest_passing_grade}.

+ ${"{0:.0f}%".format(float(cert_status['grade'])*100)}, + grade required for certificate: + ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}.

% endif % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: From 792ddb4d8647628939609fe1682a86c3b037f486 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 17:51:59 -0500 Subject: [PATCH 020/736] Show grade when certificate is generating --- lms/templates/dashboard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index cbba8bd4b5..c254eb10ed 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -243,10 +243,10 @@ %>
- % if cert_status['status'] in ('processing', 'generating'): + % if cert_status['status'] == 'processing':

Final course details are being wrapped up at this time. Your final standing will be available shortly.

- % elif cert_status['status'] in ('ready', 'notpassing'): + % elif cert_status['status'] in ('generating', 'ready', 'notpassing'):

Your final grade: ${"{0:.0f}%".format(float(cert_status['grade'])*100)}, grade required for certificate: From 2db0efa2b4b617d8f2c3a8b6b7de932330dfaa89 Mon Sep 17 00:00:00 2001 From: John Jarvis Date: Mon, 26 Nov 2012 17:57:38 -0500 Subject: [PATCH 021/736] Only show certificate criteria for notpassing students --- lms/templates/dashboard.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index c254eb10ed..d9b57ac044 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -248,9 +248,12 @@ this time. Your final standing will be available shortly.

% elif cert_status['status'] in ('generating', 'ready', 'notpassing'):

Your final grade: - ${"{0:.0f}%".format(float(cert_status['grade'])*100)}, - grade required for certificate: - ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}.

+ ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. + % if cert_status['status'] == 'notpassing': + Grade required for a certificate: + ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}. + % endif +

% endif % if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']: From 7fcf04eaf5aab47bb8d3030ab5e2aec5b4c5aabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Fri, 2 Nov 2012 14:14:13 +0200 Subject: [PATCH 022/736] add shapely --- requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 28b12404a1..ac50bd1691 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,11 +8,11 @@ lxml boto mako python-memcached -python-openid +python-openid path.py django_debug_toolbar fs -beautifulsoup +beautifulsoup beautifulsoup4 feedparser requests @@ -37,7 +37,7 @@ django-jasmine django-keyedcache django-mako django-masquerade -django-openid-auth +django-openid-auth django-robots django-ses django-storages @@ -54,3 +54,4 @@ dogstatsd-python # Taking out MySQL-python for now because it requires mysql to be installed, so breaks updates on content folks' envs. # MySQL-python sphinx +Shapely From 88c98b9a56b42b0541e8ab1dc7be6ad05ce6a5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Fri, 2 Nov 2012 14:16:37 +0200 Subject: [PATCH 023/736] added multiple regions support --- common/lib/capa/capa/responsetypes.py | 63 +++++++++++++++++---------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index b990c489b3..85efd70cee 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -23,6 +23,7 @@ import abc import os import subprocess import xml.sax.saxutils as saxutils +from shapely.geometry import Polygon, Point # specific library imports from calc import evaluator, UndefinedVariable @@ -1720,12 +1721,20 @@ class ImageResponse(LoncapaResponse): Lon-CAPA requires that each has a inside it. That doesn't make sense to me (Ike). Instead, let's have it such that should contain one or more stanzas. Each should specify - a rectangle, given as an attribute, defining the correct answer. + a rectangle(s) or region(s), given as an attribute, defining the correct answer. + + Rectangle(s) are more prioritized over regions due to simplicity and backward compatibility. + In this example regions will be ignored: + + + Regions is list of lists [region1, region2, region3, ...] where regionN is ordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. """ snippets = [{'snippet': ''' - + + + '''}] response_tag = 'imageresponse' @@ -1733,7 +1742,7 @@ class ImageResponse(LoncapaResponse): def setup_response(self): self.ielements = self.inputfields - self.answer_ids = [ie.get('id') for ie in self.ielements] + self.answer_ids = [ie.get('id') for ie in self.ielements] def get_score(self, student_answers): correct_map = CorrectMap() @@ -1743,7 +1752,7 @@ class ImageResponse(LoncapaResponse): given = student_answers[aid] # this should be a string of the form '[x,y]' correct_map.set(aid, 'incorrect') - if not given: # No answer to parse. Mark as incorrect and move on + if not given: # No answer to parse. Mark as incorrect and move on continue # parse given answer @@ -1753,29 +1762,37 @@ class ImageResponse(LoncapaResponse): 'error grading %s (input=%s)' % (aid, given)) (gx, gy) = [int(x) for x in m.groups()] - # Check whether given point lies in any of the solution rectangles - solution_rectangles = expectedset[aid].split(';') - for solution_rectangle in solution_rectangles: - # parse expected answer - # TODO: Compile regexp on file load - m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', - solution_rectangle.strip().replace(' ', '')) - if not m: - msg = 'Error in problem specification! cannot parse rectangle in %s' % ( - etree.tostring(self.ielements[aid], pretty_print=True)) - raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) - (llx, lly, urx, ury) = [int(x) for x in m.groups()] - - # answer is correct if (x,y) is within the specified rectangle - if (llx <= gx <= urx) and (lly <= gy <= ury): - correct_map.set(aid, 'correct') - break + rectangles, regions = expectedset + if rectangles[aid]: # rectangles part - for backward compatibility + # Check whether given point lies in any of the solution rectangles + solution_rectangles = rectangles[aid].split(';') + for solution_rectangle in solution_rectangles: + # parse expected answer + # TODO: Compile regexp on file load + m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', + solution_rectangle.strip().replace(' ', '')) + if not m: + msg = 'Error in problem specification! cannot parse rectangle in %s' % ( + etree.tostring(self.ielements[aid], pretty_print=True)) + raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) + (llx, lly, urx, ury) = [int(x) for x in m.groups()] + # answer is correct if (x,y) is within the specified rectangle + if (llx <= gx <= urx) and (lly <= gy <= ury): + correct_map.set(aid, 'correct') + break + else: # rectangles are more prioretized for same id + if regions[aid]: + parsed_region = json.loads(regions[aid]) + for region in parsed_region: + if Polygon(region).contains(Point(gx, gy)): + correct_map.set(aid, 'correct') + break return correct_map def get_answers(self): - return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]) - + return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]), + dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) #----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration From 406e7e29c04bcbe829752a802f675f22c1a8629b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Fri, 2 Nov 2012 17:25:18 +0200 Subject: [PATCH 024/736] added geos to requirements --- brew-formulas.txt | 15 ++++++++------- create-dev-env.sh | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/brew-formulas.txt b/brew-formulas.txt index b5b555e2a0..e06829a43a 100644 --- a/brew-formulas.txt +++ b/brew-formulas.txt @@ -1,10 +1,11 @@ -readline -sqlite -gdbm -pkg-config -gfortran -python -yuicompressor +readline +sqlite +gdbm +pkg-config +gfortran +python +yuicompressor node graphviz mysql +geos diff --git a/create-dev-env.sh b/create-dev-env.sh index e481d3fd5e..2a7f68d3b0 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -99,7 +99,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev" +APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev geos" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" From 03c338d6e3a64235fbc8f7f32da060b5922d704d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 18:26:16 +0200 Subject: [PATCH 025/736] added geos library for linux --- create-dev-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create-dev-env.sh b/create-dev-env.sh index 2a7f68d3b0..5edc765e4f 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -99,7 +99,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev geos" +APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" From b4fb0cc463fa58ba4a8bc2f0bbef5da1a7c9e0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 18:34:38 +0200 Subject: [PATCH 026/736] fixed line length --- common/lib/capa/capa/responsetypes.py | 41 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 85efd70cee..730cdb12cf 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1718,23 +1718,33 @@ class ImageResponse(LoncapaResponse): which produces an [x,y] coordinate pair. The click is correct if it falls within a region specified. This region is a union of rectangles. - Lon-CAPA requires that each has a inside it. That - doesn't make sense to me (Ike). Instead, let's have it such that - should contain one or more stanzas. Each should specify - a rectangle(s) or region(s), given as an attribute, defining the correct answer. + Lon-CAPA requires that each has a inside it. + That doesn't make sense to me (Ike). Instead, let's have it such that + should contain one or more stanzas. + Each should specify a rectangle(s) or region(s), given as an + attribute, defining the correct answer. - Rectangle(s) are more prioritized over regions due to simplicity and backward compatibility. - In this example regions will be ignored: - + Rectangle(s) are more prioritized over regions due to simplicity and + backward compatibility. In this example regions will be ignored: + - Regions is list of lists [region1, region2, region3, ...] where regionN is ordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + Regions is list of lists [region1, region2, region3, ...] where regionN + is ordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. """ snippets = [{'snippet': ''' - - - - - + + + + + '''}] response_tag = 'imageresponse' @@ -1748,8 +1758,9 @@ class ImageResponse(LoncapaResponse): correct_map = CorrectMap() expectedset = self.get_answers() - for aid in self.answer_ids: # loop through IDs of fields in our stanza - given = student_answers[aid] # this should be a string of the form '[x,y]' + for aid in self.answer_ids: # loop through IDs of + # fields in our stanza + given = student_answers[aid] # this should be a string of the form '[x,y]' correct_map.set(aid, 'incorrect') if not given: # No answer to parse. Mark as incorrect and move on From 647d1514aa7d4433595943727ecec57ddd4a71f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 18:38:46 +0200 Subject: [PATCH 027/736] support both regions and rectangles --- common/lib/capa/capa/responsetypes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 730cdb12cf..74c801b450 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1724,14 +1724,15 @@ class ImageResponse(LoncapaResponse): Each should specify a rectangle(s) or region(s), given as an attribute, defining the correct answer. - Rectangle(s) are more prioritized over regions due to simplicity and - backward compatibility. In this example regions will be ignored: Regions is list of lists [region1, region2, region3, ...] where regionN is ordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + + Returns: + True, if click is inside any region or rectangle. Otherwise False. """ snippets = [{'snippet': ''' Date: Tue, 27 Nov 2012 13:09:58 +0200 Subject: [PATCH 028/736] support for single list syntax in regions --- common/lib/capa/capa/responsetypes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 74c801b450..000d77d655 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1758,9 +1758,10 @@ class ImageResponse(LoncapaResponse): def get_score(self, student_answers): correct_map = CorrectMap() expectedset = self.get_answers() - + # import ipdb; ipdb.set_trace() for aid in self.answer_ids: # loop through IDs of # fields in our stanza + # import ipdb; ipdb.set_trace() given = student_answers[aid] # this should be a string of the form '[x,y]' correct_map.set(aid, 'incorrect') @@ -1795,10 +1796,16 @@ class ImageResponse(LoncapaResponse): break if regions[aid]: parsed_region = json.loads(regions[aid]) - for region in parsed_region: - if Polygon(region).contains(Point(gx, gy)): - correct_map.set(aid, 'correct') - break + if parsed_region: + if type(parsed_region[0][0]) != list: + # we have [[1,2],[3,4],[5,6] - single region + # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]] + # or [[[1,2],[3,4],[5,6]] - multiple regions syntax + parsed_region = [parsed_region] + for region in parsed_region: + if Polygon(region).contains(Point(gx, gy)): + correct_map.set(aid, 'correct') + break return correct_map def get_answers(self): From 5723672e490c4d19ddedbcb6e6ea40cce2e6f822 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 13:16:47 +0200 Subject: [PATCH 029/736] speed improvements --- common/lib/capa/capa/responsetypes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 000d77d655..aeabacef59 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1794,7 +1794,9 @@ class ImageResponse(LoncapaResponse): if (llx <= gx <= urx) and (lly <= gy <= ury): correct_map.set(aid, 'correct') break - if regions[aid]: + # import ipdb; ipdb.set_trace() + if correct_map[aid]['correctness'] != 'correct' and regions[aid]: + import ipdb; ipdb.set_trace() parsed_region = json.loads(regions[aid]) if parsed_region: if type(parsed_region[0][0]) != list: From 72f9358f64d055d46466e1adf836f368aaf4182d Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 13:18:37 +0200 Subject: [PATCH 030/736] tests for imageresponse regions --- .../capa/tests/test_files/imageresponse.xml | 19 ++++++++++++ .../lib/capa/capa/tests/test_responsetypes.py | 30 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/tests/test_files/imageresponse.xml b/common/lib/capa/capa/tests/test_files/imageresponse.xml index 34dba37e3b..c4590b211f 100644 --- a/common/lib/capa/capa/tests/test_files/imageresponse.xml +++ b/common/lib/capa/capa/tests/test_files/imageresponse.xml @@ -18,4 +18,23 @@ Hello

Use conservation of energy.

+ + + + + + + +Click on either of the two positions as discussed previously. + +Click on either of the two positions as discussed previously. + + +Click on either of the two positions as discussed previously. + +

Use conservation of energy.

+
+
+ + diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index bcac555b5e..3564ad3fe8 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -52,24 +52,52 @@ class ImageResponseTest(unittest.TestCase): def test_ir_grade(self): imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml" test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': '(490,11)-(556,98)', + # testing regions only + correct_answers = { + #regions + '1_2_1': '(490,11)-(556,98)', '1_2_2': '(242,202)-(296,276)', '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', + #testing regions and rectanges + '1_3_1': '(490,11)-(556,98)', + '1_3_2': '(242,202)-(296,276)', + '1_3_3': '(490,11)-(556,98);(242,202)-(296,276)', + '1_3_4': '(490,11)-(556,98);(242,202)-(296,276)', + '1_3_5': '(490,11)-(556,98);(242,202)-(296,276)', } test_answers = {'1_2_1': '[500,20]', '1_2_2': '[250,300]', '1_2_3': '[500,20]', '1_2_4': '[250,250]', '1_2_5': '[10,10]', + + '1_3_1': '[500,20]', + '1_3_2': '[15,15]', + '1_3_3': '[500,20]', + '1_3_4': '[115,115]', + '1_3_5': '[15,15]', + '1_3_6': '[20,20]', + '1_3_7': '[20,20]', } + + # regions self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect') + # regions and rectangles + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_2'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_3'), 'incorrect') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_4'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_5'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_6'), 'incorrect') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_7'), 'correct') + class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): From adef5d6f7eb1af0f12fdfc543d3822ad91253cad Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 14:11:11 +0200 Subject: [PATCH 031/736] polygon is created from points via convex_hull --- common/lib/capa/capa/responsetypes.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index aeabacef59..ae5d764568 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -23,7 +23,7 @@ import abc import os import subprocess import xml.sax.saxutils as saxutils -from shapely.geometry import Polygon, Point +from shapely.geometry import Point, MultiPoint # specific library imports from calc import evaluator, UndefinedVariable @@ -1796,16 +1796,20 @@ class ImageResponse(LoncapaResponse): break # import ipdb; ipdb.set_trace() if correct_map[aid]['correctness'] != 'correct' and regions[aid]: - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() parsed_region = json.loads(regions[aid]) if parsed_region: if type(parsed_region[0][0]) != list: - # we have [[1,2],[3,4],[5,6] - single region - # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]] - # or [[[1,2],[3,4],[5,6]] - multiple regions syntax + # we have [[1,2],[3,4],[5,6]] - single region + # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] + # or [[[1,2],[3,4],[5,6]]] - multiple regions syntax parsed_region = [parsed_region] + # if aid =='1_3_6': + # import ipdb; ipdb.set_trace() for region in parsed_region: - if Polygon(region).contains(Point(gx, gy)): + polygon = MultiPoint(region).convex_hull + if (polygon.type == 'Polygon' and + polygon.contains(Point(gx, gy))): correct_map.set(aid, 'correct') break return correct_map From 896e922858adfbda5e3117186d154292c1865103 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 14:27:19 +0200 Subject: [PATCH 032/736] cleaning and documeting --- common/lib/capa/capa/responsetypes.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index ae5d764568..20e7c43577 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1726,10 +1726,14 @@ class ImageResponse(LoncapaResponse): + regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/> Regions is list of lists [region1, region2, region3, ...] where regionN - is ordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. + + If there is only one region in the list, simpler notation can be used: + regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly + setting outer list) Returns: True, if click is inside any region or rectangle. Otherwise False. @@ -1743,9 +1747,9 @@ class ImageResponse(LoncapaResponse): rectangle="(10,10)-(20,30);(12,12)-(40,60)" /> + regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/> + regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
'''}] response_tag = 'imageresponse' @@ -1758,16 +1762,12 @@ class ImageResponse(LoncapaResponse): def get_score(self, student_answers): correct_map = CorrectMap() expectedset = self.get_answers() - # import ipdb; ipdb.set_trace() for aid in self.answer_ids: # loop through IDs of # fields in our stanza - # import ipdb; ipdb.set_trace() given = student_answers[aid] # this should be a string of the form '[x,y]' - correct_map.set(aid, 'incorrect') if not given: # No answer to parse. Mark as incorrect and move on continue - # parse given answer m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) if not m: @@ -1794,9 +1794,7 @@ class ImageResponse(LoncapaResponse): if (llx <= gx <= urx) and (lly <= gy <= ury): correct_map.set(aid, 'correct') break - # import ipdb; ipdb.set_trace() if correct_map[aid]['correctness'] != 'correct' and regions[aid]: - # import ipdb; ipdb.set_trace() parsed_region = json.loads(regions[aid]) if parsed_region: if type(parsed_region[0][0]) != list: @@ -1804,8 +1802,6 @@ class ImageResponse(LoncapaResponse): # instead of [[[1,2],[3,4],[5,6], [[1,2],[3,4],[5,6]]] # or [[[1,2],[3,4],[5,6]]] - multiple regions syntax parsed_region = [parsed_region] - # if aid =='1_3_6': - # import ipdb; ipdb.set_trace() for region in parsed_region: polygon = MultiPoint(region).convex_hull if (polygon.type == 'Polygon' and From 95151ad33a6deabba01c9b1cfab7acd789a2537c Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 14:27:44 +0200 Subject: [PATCH 033/736] tests updates for imageresponse --- .../capa/tests/test_files/imageresponse.xml | 4 ++-- .../lib/capa/capa/tests/test_responsetypes.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/common/lib/capa/capa/tests/test_files/imageresponse.xml b/common/lib/capa/capa/tests/test_files/imageresponse.xml index c4590b211f..41c9f01218 100644 --- a/common/lib/capa/capa/tests/test_files/imageresponse.xml +++ b/common/lib/capa/capa/tests/test_files/imageresponse.xml @@ -22,14 +22,14 @@ Hello

- + Click on either of the two positions as discussed previously. Click on either of the two positions as discussed previously. - + Click on either of the two positions as discussed previously.

Use conservation of energy.

diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 3564ad3fe8..be734bdb88 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -54,18 +54,20 @@ class ImageResponseTest(unittest.TestCase): test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) # testing regions only correct_answers = { - #regions - '1_2_1': '(490,11)-(556,98)', + #regions + '1_2_1': '(490,11)-(556,98)', '1_2_2': '(242,202)-(296,276)', '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', #testing regions and rectanges - '1_3_1': '(490,11)-(556,98)', - '1_3_2': '(242,202)-(296,276)', - '1_3_3': '(490,11)-(556,98);(242,202)-(296,276)', - '1_3_4': '(490,11)-(556,98);(242,202)-(296,276)', - '1_3_5': '(490,11)-(556,98);(242,202)-(296,276)', + '1_3_1': 'rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_2': 'rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"', + '1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"', + '1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"', } test_answers = {'1_2_1': '[500,20]', '1_2_2': '[250,300]', @@ -79,7 +81,7 @@ class ImageResponseTest(unittest.TestCase): '1_3_4': '[115,115]', '1_3_5': '[15,15]', '1_3_6': '[20,20]', - '1_3_7': '[20,20]', + '1_3_7': '[20,15]', } # regions From b0a85fde540941be127f317cd4f131ecbbdc4ddb Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 14:29:04 +0200 Subject: [PATCH 034/736] fix line widths --- .../lib/capa/capa/tests/test_responsetypes.py | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index be734bdb88..9eecef3986 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -54,34 +54,37 @@ class ImageResponseTest(unittest.TestCase): test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) # testing regions only correct_answers = { - #regions - '1_2_1': '(490,11)-(556,98)', - '1_2_2': '(242,202)-(296,276)', - '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', - '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', - '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', - #testing regions and rectanges - '1_3_1': 'rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_2': 'rectangle="(490,11)-(556,98)" regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"', - '1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"', - '1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"', + #regions + '1_2_1': '(490,11)-(556,98)', + '1_2_2': '(242,202)-(296,276)', + '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', + '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', + '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', + #testing regions and rectanges + '1_3_1': 'rectangle="(490,11)-(556,98)" \ + regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_2': 'rectangle="(490,11)-(556,98)" \ + regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', + '1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"', + '1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"', + '1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"', } - test_answers = {'1_2_1': '[500,20]', - '1_2_2': '[250,300]', - '1_2_3': '[500,20]', - '1_2_4': '[250,250]', - '1_2_5': '[10,10]', + test_answers = { + '1_2_1': '[500,20]', + '1_2_2': '[250,300]', + '1_2_3': '[500,20]', + '1_2_4': '[250,250]', + '1_2_5': '[10,10]', - '1_3_1': '[500,20]', - '1_3_2': '[15,15]', - '1_3_3': '[500,20]', - '1_3_4': '[115,115]', - '1_3_5': '[15,15]', - '1_3_6': '[20,20]', - '1_3_7': '[20,15]', + '1_3_1': '[500,20]', + '1_3_2': '[15,15]', + '1_3_3': '[500,20]', + '1_3_4': '[115,115]', + '1_3_5': '[15,15]', + '1_3_6': '[20,20]', + '1_3_7': '[20,15]', } # regions From dcd52801ef341f4b65f4f7e5fba5ecec772f3f41 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Tue, 27 Nov 2012 16:10:51 +0200 Subject: [PATCH 035/736] case when user sends less than 3 points, and tests for this case --- common/lib/capa/capa/chem/miller.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index f1482de80f..77c10dd350 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -253,13 +253,15 @@ def grade(user_input, correct_answer): points = [map(float, p) for p in user_answer['points']] + if len(points) < 3: + return False + # round point to closes 0.05 value points = [round0_25(point) for point in points] points = [np.array(point) for point in points] # print miller(points), (correct_answer['miller'].replace(' ', ''), # negative(correct_answer['miller']).replace(' ', '')) - if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')): return True @@ -269,6 +271,18 @@ def grade(user_input, correct_answer): class Test_Crystallography_Miller(unittest.TestCase): ''' Tests for crystallography grade function.''' + def test_empty_points(self): + user_input = '{"lattice": "bcc", "points": []}' + self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_only_one_point(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}' + self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_only_two_points(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}' + self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + def test_1(self): user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' self.assertTrue(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) From c4377484d5d8dc50ce656a7b83827f71322aa306 Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Tue, 27 Nov 2012 11:39:33 -0500 Subject: [PATCH 036/736] You need to fill out your full name but it's not marked with a * as required. --- lms/templates/signup_modal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/signup_modal.html b/lms/templates/signup_modal.html index 96b2e33ac3..22a4a93499 100644 --- a/lms/templates/signup_modal.html +++ b/lms/templates/signup_modal.html @@ -26,7 +26,7 @@ - + % else:

Welcome ${extauth_email}


From d4f5f173816b2609942ad0edc98278fd0a621260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 27 Nov 2012 19:06:03 -0500 Subject: [PATCH 037/736] Remove unused askbot/discussion sass stylesheets The unused styles are referencing images that no longer exisit, breaking collectstatic and therefore tests. --- lms/static/sass/course.scss | 14 +- .../sass/course/discussion/_answers.scss | 156 - .../course/discussion/_askbot-original.scss | 2710 ----------------- .../sass/course/discussion/_badges.scss | 88 - .../sass/course/discussion/_discussion.scss | 82 - lms/static/sass/course/discussion/_forms.scss | 184 -- .../sass/course/discussion/_modals.scss | 33 - .../sass/course/discussion/_profile.scss | 119 - .../course/discussion/_question-view.scss | 377 --- .../sass/course/discussion/_questions.scss | 264 -- .../sass/course/discussion/_sidebar.scss | 319 -- lms/static/sass/course/discussion/_tags.scss | 44 - 12 files changed, 1 insertion(+), 4389 deletions(-) delete mode 100644 lms/static/sass/course/discussion/_answers.scss delete mode 100644 lms/static/sass/course/discussion/_askbot-original.scss delete mode 100644 lms/static/sass/course/discussion/_badges.scss delete mode 100644 lms/static/sass/course/discussion/_discussion.scss delete mode 100644 lms/static/sass/course/discussion/_forms.scss delete mode 100644 lms/static/sass/course/discussion/_modals.scss delete mode 100644 lms/static/sass/course/discussion/_profile.scss delete mode 100644 lms/static/sass/course/discussion/_question-view.scss delete mode 100644 lms/static/sass/course/discussion/_questions.scss delete mode 100644 lms/static/sass/course/discussion/_sidebar.scss delete mode 100644 lms/static/sass/course/discussion/_tags.scss diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss index 924b4852c0..acd735d25e 100644 --- a/lms/static/sass/course.scss +++ b/lms/static/sass/course.scss @@ -47,17 +47,5 @@ // instructor @import "course/instructor/instructor"; -// Askbot / Discussion styles -// TODO: Get rid of askbot-specific styles. -@import "course/discussion/askbot-original"; -@import "course/discussion/discussion"; -@import "course/discussion/sidebar"; -@import "course/discussion/questions"; -@import "course/discussion/tags"; -@import "course/discussion/question-view" ; -@import "course/discussion/answers"; -@import "course/discussion/forms"; +// discussion @import "course/discussion/form-wmd-toolbar"; -@import "course/discussion/modals"; -@import "course/discussion/profile"; -@import "course/discussion/badges"; diff --git a/lms/static/sass/course/discussion/_answers.scss b/lms/static/sass/course/discussion/_answers.scss deleted file mode 100644 index c4aa683726..0000000000 --- a/lms/static/sass/course/discussion/_answers.scss +++ /dev/null @@ -1,156 +0,0 @@ -// Styles for individual answers - -div.answer-controls { - @include box-sizing(border-box); - display: inline-block; - margin: 0 0 15px; - padding-left: flex-grid(1.1); - width: 100%; - - - div.answer-count { - display: inline-block; - float: left; - - h1 { - margin-bottom: 0; - font-size: em(24); - font-weight: 100; - } - } - - div.answer-sort { - float: right; - margin-left: flex-gutter(); - - nav { - float: right; - margin-top: 10px; - - a { - &.on span{ - font-weight: bold; - } - - &:before { - content: '|'; - color: #ccc; - font-size: 16px; - } - } - } - } -} - -div.answer-block { - @extend div.question-header; - border-top: #ddd 1px solid; - display: inline-block; - float: left; - padding-top: 20px; - width: 100%; - - img.answer-img-accept { - margin: 10px 0px 10px 11px; - } - - div.answer-container { - @extend div.question-container; - - div.answer-content { - @extend div.question-content; - - div.answer-body { - @extend div.question-body; - } - } - } - - div.meta-bar { - div.answer-actions { - @extend div.question-actions; - } - } - - div.answered-by-owner { - p { - font-style: italic; - color: #656565; - } - - div.comments-container { - color: #555; - } - } - - div.accepted-answer { - p { - color:#000; - } - } - - div.deleted { - p { - color: $pink; - } - } - - img.answer-img-accept { - opacity: 0.7; - } -} - -div.paginator { - @extend div.answer-block; - text-align: center; - padding: 20px 0; - - span { - @include border-radius(3px); - background: #eee; - margin: 0 5px; - padding: 4px 10px; - - &.curr { - background: none; - color: $pink; - font-weight: bold; - } - - &.next, &.prev { - @extend .light-button; - } - - a { - color: #555; - text-decoration: none; - border-bottom: none; - } - } -} - -div.answer-own { - border-top: 1px solid #eee; - overflow:hidden; - padding-left: flex-grid(1.2); - padding-top: 10px; -} - -div.answer-actions { - margin: 0; - padding:8px 0 8px 8px; - text-align: right; - border-top: 1px solid #efefef; - - span.sep { - color: $border-color; - } - - a { - cursor: pointer; - text-decoration: none; - @extend a:link; - font-size: em(14); - } -} - diff --git a/lms/static/sass/course/discussion/_askbot-original.scss b/lms/static/sass/course/discussion/_askbot-original.scss deleted file mode 100644 index a8bb66acfe..0000000000 --- a/lms/static/sass/course/discussion/_askbot-original.scss +++ /dev/null @@ -1,2710 +0,0 @@ -// original Askbot styles - -// body { -// background: #fff; -// font-size: 14px; -// line-height: 150%; -// margin: 0; -// padding: 0; -// color: #000; -// font-family: arial; } - -// div { -// margin: 0 auto; -// padding: 0; } - -// h1, h2, h3, h4, h5, h6, ul, li, dl, dt, dd, form, img, p { -// margin: 0; -// padding: 0; -// border: none; } - -// label { -// vertical-align: middle; } - -// hr { -// border: none; -// border-top: 1px dashed #ccccce; } - -// input, select { -// vertical-align: middle; -// font-family: trebuchet ms,"segoe ui",helvetica,tahoma,verdana,mingliu,pmingliu,arial,sans-serif; -// margin-left: 0px; } - -// textarea:focus, input:focus { -// outline: none; } - -// iframe { -// border: none; } - -// p { -// font-size: 14px; -// line-height: 140%; -// margin-bottom: 6px; } - -// a { -// color: #1b79bd; -// text-decoration: none; -// cursor: pointer; } - -// h2 { -// font-size: 21px; -// padding: 3px 0 3px 5px; } - -// h3 { -// font-size: 19px; -// padding: 3px 0 3px 5px; } - -// ul { -// list-style: disc; -// margin-left: 20px; -// padding-left: 0px; -// margin-bottom: 1em; } - -// ol { -// list-style: decimal; -// margin-left: 30px; -// margin-bottom: 1em; -// padding-left: 0px; } - -// td ul { -// vertical-align: middle; } - -// li input { -// margin: 3px 3px 4px 3px; } - -// pre { -// font-family: consolas, monaco, liberation mono, lucida console, monospace; -// font-size: 100%; -// margin-bottom: 10px; -// background-color: #f5f5f5; -// padding-left: 5px; -// padding-top: 5px; -// padding-bottom: 20px; } - -// code { -// font-family: consolas, monaco, liberation mono, lucida console, monospace; -// font-size: 100%; } - -// blockquote { -// margin-bottom: 10px; -// margin-right: 15px; -// padding: 10px 0px 1px 10px; -// background-color: #f5f5f5; } - -// * html { -// .clearfix, .paginator { -// height: 1; -// overflow: visible; } } - -// +html { -// .clearfix, .paginator { -// min-height: 1%; } } - -// .clearfix:after, .paginator:after { -// clear: both; -// content: "."; -// display: block; -// height: 0; -// visibility: hidden; } - -// .badges a { -// color: #763333; -// text-decoration: underline; } - -// a:hover { -// text-decoration: underline; } - -.badge-context-toggle.active { - cursor: pointer; - text-decoration: underline; } - -// h1 { -// font-size: 24px; -// padding: 10px 0 5px 0px; } - -body.user-messages { - margin-top: 2.4em; } - -// .left { -// float: left; } - -// .right { -// float: right; } - -// .clean { -// clear: both; } - -// .center { -// margin: 0 auto; -// padding: 0; } - -.notify { - position: fixed; - top: 0px; - left: 0px; - width: 100%; - z-index: 100; - padding: 0; - text-align: center; - background-color: #f5dd69; - border-top: #fff 1px solid; - - p.notification { - margin-top: 6px; - margin-bottom: 6px; - font-size: 16px; - color: #424242; } } - -#closenotify { - position: absolute; - right: 5px; - top: 7px; - color: #735005; - text-decoration: none; - line-height: 18px; - background: -6px -5px url(../default/media/images/sprites.png) no-repeat; - cursor: pointer; - width: 20px; - height: 20px; - &:hover { - background: -26px -5px url(../default/media/images/sprites.png) no-repeat; } } - -#header { - margin-top: 0px; - background: #16160f; } - -/*.content-wrapper { - width: 960px; - margin: auto; - position: relative; }*/ - -#logo img { - padding: 5px 0px 5px 0px; - height: 75px; - width: auto; - float: left; } - -#usertoolsnav { - height: 20px; - padding-bottom: 5px; - a { - height: 35px; - text-align: right; - margin-left: 20px; - text-decoration: underline; - color: #d0e296; - font-size: 16px; - &:first-child { - margin-left: 0; } - &#ab-responses { - margin-left: 3px; } } - .user-info, .user-micro-info { - color: #b5b593; } - a img { - vertical-align: middle; - margin-bottom: 2px; } - .user-info a { - margin: 0; - text-decoration: none; } } - -#metanav { - float: right; - a { - color: #e2e2ae; - padding: 0px 0px 0px 35px; - height: 25px; - line-height: 30px; - margin: 5px 0px 0px 10px; - font-size: 18px; - font-weight: 100; - text-decoration: none; - display: block; - float: left; - &:hover { - text-decoration: underline; } - &.on { - font-weight: bold; - color: #fff; - text-decoration: none; } - &.special { - font-size: 18px; - color: #b02b2c; - font-weight: bold; - text-decoration: none; - &:hover { - text-decoration: underline; } } } - #navtags { - background: -50px -5px url(../default/media/images/sprites.png) no-repeat; } - #navusers { - background: -125px -5px url(../default/media/images/sprites.png) no-repeat; } - #navbadges { - background: -210px -5px url(../default/media/images/sprites.png) no-repeat; } } - -// #header { -// &.with-logo #usertoolsnav { -// position: absolute; -// bottom: 0; -// right: 0px; } -// &.without-logo { -// #usertoolsnav { -// float: left; -// margin-top: 7px; } -// #metanav { -// margin-bottom: 7px; } } } - -// #secondaryheader { -// height: 55px; -// background: #e9e9e1; -// border-bottom: #d3d3c2 1px solid; -// border-top: #fcfcfc 1px solid; -// margin-bottom: 10px; -// font-family: 'yanone kaffeesatz',sans-serif; -// #homebutton { -// border-right: #afaf9e 1px solid; -// background: -6px -36px url(../default/media/images/sprites.png) no-repeat; -// height: 55px; -// width: 43px; -// display: block; -// float: left; -// &:hover { -// background: -51px -36px url(../default/media/images/sprites.png) no-repeat; } } -// #scopewrapper { -// width: 688px; -// float: left; -// a { -// display: block; -// float: left; } -// .scope-selector { -// font-size: 21px; -// color: #5a5a4b; -// height: 55px; -// line-height: 55px; -// margin-left: 24px; } -// .on { -// background: url(../default/media/images/scopearrow.png) no-repeat center bottom; } -// .ask-message { -// font-size: 24px; } } } - -#searchbar { - display: inline-block; - background-color: #fff; - width: 412px; - border: 1px solid #c9c9b5; - float: right; - height: 42px; - margin: 6px 0px 0px 15px; - .searchinput, .searchinputcancelable { - font-size: 30px; - height: 40px; - font-weight: 300; - background: #fff; - border: 0px; - color: #484848; - padding-left: 10px; - font-family: arial; - vertical-align: middle; } - .searchinput { - width: 352px; } - .searchinputcancelable { - width: 317px; } - .logoutsearch { - width: 337px; } - .searchbtn { - font-size: 10px; - color: #666; - background-color: #eee; - height: 42px; - border: #fff 1px solid; - line-height: 22px; - text-align: center; - float: right; - margin: 0px; - width: 48px; - background: -98px -36px url(../default/media/images/sprites.png) no-repeat; - cursor: pointer; - &:hover { - background: -146px -36px url(../default/media/images/sprites.png) no-repeat; } } - .cancelsearchbtn { - font-size: 30px; - color: #ce8888; - background: #fff; - height: 42px; - border: 0px; - border-left: #deded0 1px solid; - text-align: center; - width: 35px; - cursor: pointer; - &:hover { - color: #d84040; } } } - -body.anon #searchbar { - width: 500px; - .searchinput { - width: 440px; } - .searchinputcancelable { - width: 405px; } } - -#askbutton { - background: url(../default/media/images/bigbutton.png) repeat-x bottom; - line-height: 44px; - text-align: center; - width: 200px; - height: 42px; - font-size: 23px; - color: #4a757f; - margin-top: 7px; - float: right; - text-transform: uppercase; - border-radius: 5px; - -ms-border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - -khtml-border-radius: 5px; - -webkit-box-shadow: 1px 1px 2px #636363; - -moz-box-shadow: 1px 1px 2px #636363; - box-shadow: 1px 1px 2px #636363; - &:hover { - text-decoration: none; - background: url(../default/media/images/bigbutton.png) repeat-x top; - text-shadow: 0px 1px 0px #c6d9dd; - -moz-text-shadow: 0px 1px 0px #c6d9dd; - -webkit-text-shadow: 0px 1px 0px #c6d9dd; } } - -/*#contentleft { - width: 730px; - float: left; - position: relative; - padding-bottom: 10px; } - -#contentright { - width: 200px; - float: right; - padding: 0 0px 10px 0px; }*/ - -#contentfull { - float: left; - width: 960px; } - -.box { - /*background: #fff;*/ - /*padding: 4px 0px 10px 0px;*/ - /*width: 200px;*/ - p { - margin-bottom: 4px; - &.info-box-follow-up-links { - text-align: right; - margin: 0; } } - h2 { - // padding-left: 0; - // /*background: #eceeeb;*/ - // height: 30px; - // line-height: 30px; - // /*text-align: right;*/ - // /*font-size: 18px !important;*/ - // // font-weight: normal; - // // color: #656565; - // /*padding-right: 10px;*/ - // /*margin-bottom: 10px;*/ - // /*font-family: 'yanone kaffeesatz',sans-serif;*/ - } - // h3 { - // /*color: #4a757f;*/ - // /*font-size: 18px;*/ - // text-align: left; - // font-weight: normal; - // /*font-family: 'yanone kaffeesatz',sans-serif;*/ - // padding-left: 0px; } - // .contributorback { - // background: #eceeeb url(../default/media/images/contributorsback.png) no-repeat center left; } - // label { - // color: #707070; - // font-size: 15px; - // display: block; - // float: right; - // text-align: left; - // font-family: 'yanone kaffeesatz',sans-serif; - // width: 80px; - // margin-right: 18px; } - // #displaytagfiltercontrol label { - // width: 160px; } - // ul { - // margin-left: 22px; } - // li { - // list-style-type: disc; - // font-size: 13px; - // line-height: 20px; - // margin-bottom: 10px; - // color: #707070; } - // ul.tags { - // list-style: none; - // margin: 0; - // padding: 0; - // line-height: 170%; - // display: block; } - // #displaytagfiltercontrol p label { - // color: #707070; - // font-size: 15px; } - /*.inputs { - #interestingtaginput, #ignoredtaginput { - width: 153px; - padding-left: 5px; - border: #c9c9b5 1px solid; - height: 25px; } - #interestingtagadd, #ignoredtagadd { - background: url(../default/media/images/small-button-blue.png) repeat-x top; - border: 0; - color: #4a757f; - font-weight: bold; - font-size: 12px; - width: 30px; - height: 27px; - margin-top: -2px; - cursor: pointer; - border-radius: 4px; - -ms-border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; } - #interestingtagadd:hover, #ignoredtagadd:hover { - background: url(../default/media/images/small-button-blue.png) repeat-x bottom; } }*/ - // img.gravatar { - // margin: 1px; } - // a { - // &.followed, &.follow { - // background: url(../default/media/images/medium-button.png) top repeat-x; - // height: 34px; - // line-height: 34px; - // text-align: center; - // border: 0; - // font-family: 'yanone kaffeesatz',sans-serif; - // color: #4a757f; - // font-weight: normal; - // font-size: 21px; - // margin-top: 3px; - // display: block; - // width: 120px; - // text-decoration: none; - // border-radius: 4px; - // -ms-border-radius: 4px; - // -moz-border-radius: 4px; - // -webkit-border-radius: 4px; - // -khtml-border-radius: 4px; - // -webkit-box-shadow: 1px 1px 2px #636363; - // -moz-box-shadow: 1px 1px 2px #636363; - // box-shadow: 1px 1px 2px #636363; - // margin: 0 auto; - // padding: 0; } - // &.followed:hover, &.follow:hover { - // text-decoration: none; - // background: url(../default/media/images/medium-button.png) bottom repeat-x; - // text-shadow: 0px 1px 0px #c6d9dd; - // -moz-text-shadow: 0px 1px 0px #c6d9dd; - // -webkit-text-shadow: 0px 1px 0px #c6d9dd; } - // &.followed { - // div.unfollow { - // display: none; } - // &:hover div { - // display: none; - // &.unfollow { - // display: inline; - // color: #a05736; } } } } - // .favorite-number { - // padding: 5px 0 0 5px; - // font-size: 100%; - // font-family: arial; - // font-weight: bold; - // color: #777; - // text-align: center; } - // .notify-sidebar #question-subscribe-sidebar { - // margin: 7px 0 0 3px; } - } - -//.statswidget p { - // color: #707070; - // font-size: 16px; - // border-bottom: #cccccc 1px solid; - // font-size: 13px; - // strong { - // float: right; - // padding-right: 10px; } } - -// .questions-related { -// word-wrap: break-word; -// p { -// line-height: 20px; -// padding: 4px 0px 4px 0px; -// font-size: 16px; -// font-weight: normal; -// border-bottom: #cccccc 1px solid; } -// a { -// font-size: 13px; } } - -// #tips { -// li { -// color: #707070; -// font-size: 13px; -// list-style-image: url(../default/media/images/tips.png); } -// a { -// font-size: 16px; } } - -// #markdownhelp { -// li { -// color: #707070; -// font-size: 13px; } -// a { -// font-size: 16px; } } - -// .tabbar { - // background-color: #eff5f6; - // height: 30px; - // margin-bottom: 3px; - // margin-top: 3px; - // float: right; - // font-family: georgia,serif; - // font-size: 16px; - // border-radius: 5px; - // -ms-border-radius: 5px; - // -moz-border-radius: 5px; - // -webkit-border-radius: 5px; - // -khtml-border-radius: 5px; - // h2 { - // float: left; } } - -// .tabsa, .tabsc { - // float: right; - // position: relative; - // display: block; - // height: 20px; } - -// .tabsa { - // float: right; } - -// .tabsc { - // float: left; } - -// .tabsa a, .tabsc a { - // border-left: 1px solid #d0e1e4; - // color: #7ea9b3; - // display: block; - // float: left; - // height: 20px; - // line-height: 20px; - // padding: 4px 7px 4px 7px; - // text-decoration: none; } - -// .tabsa a.on, .tabsc a.on, .tabsa a:hover, .tabsc a:hover { - // color: #4a757f; } - -// .tabsa .label, .tabsc .label { - // float: left; - // color: #646464; - // margin-top: 4px; - // margin-right: 5px; } - -// .main-page .tabsa .label { - // margin-left: 8px; } - -// .tabsb a { - // background: #eee; - // border: 1px solid #eee; - // color: #777; - // display: block; - // float: left; - // height: 22px; - // line-height: 28px; - // margin: 5px 0px 0 4px; - // padding: 0 11px 0 11px; - // text-decoration: none; } - -// .tabsc .first { - // border: none; } - -// .rss { - // float: right; - // font-size: 16px; - // color: #f57900; - // margin: 5px 0px 3px 7px; - // width: 52px; - // padding-left: 2px; - // padding-top: 3px; - // background: white url(../default/media/images/feed-icon-small.png) no-repeat center right; - // float: right; - // font-family: georgia,serif; - // font-size: 16px; - // &:hover { - // color: #f4a731 !important; } } - -// #questioncount { -// font-weight: bold; -// font-size: 23px; -// color: #7ea9b3; -// width: 200px; -// float: left; -// margin-bottom: 8px; -// padding-top: 6px; -// font-family: 'yanone kaffeesatz',sans-serif; } - -// #listsearchtags { -// float: left; -// margin-top: 3px; -// color: #707070; -// font-size: 16px; -// font-family: 'yanone kaffeesatz',sans-serif; } - -// ul#searchtags { -// margin-left: 10px; -// float: right; -// padding-top: 2px; } - -// .search-tips { -// font-size: 16px; -// line-height: 17px; -// color: #707070; -// margin: 5px 0 10px 0; -// padding: 0px; -// float: left; -// font-family: 'yanone kaffeesatz',sans-serif; -// a { -// text-decoration: underline; -// color: #1b79bd; } } - -// #question-list { -// float: left; -// position: relative; -// background-color: #fff; -// padding: 0; -// width: 100%; } - -// .short-summary { -// position: relative; -// filter: inherit; -// padding: 10px; -// border-bottom: 1px solid #dddbce; -// margin-bottom: 1px; -// overflow: hidden; -// width: 710px; -// float: left; -// background: url(../default/media/images/summary-background.png) repeat-x; -// h2 { -// font-size: 24px; -// font-weight: normal; -// line-height: 26px; -// padding-left: 0; -// margin-bottom: 6px; -// display: block; -// font-family: 'yanone kaffeesatz',sans-serif; } -// a { -// color: #464646; } -// .userinfo { -// text-align: right; -// line-height: 16px; -// font-family: arial; -// padding-right: 4px; -// .relativetime { -// font-size: 11px; -// clear: both; -// font-weight: normal; -// color: #555; } } -// span.anonymous { -// font-size: 11px; -// clear: both; -// font-weight: normal; -// color: #555; } -// .userinfo a { -// font-weight: bold; -// font-size: 11px; } -// .counts { -// float: right; -// margin: 4px 0 0 5px; -// font-family: 'yanone kaffeesatz',sans-serif; -// .item-count { -// padding: 0px 5px 0px 5px; -// font-size: 25px; -// font-family: 'yanone kaffeesatz',sans-serif; } -// .votes div, .views div, .answers div, .favorites div { -// margin-top: 3px; -// font-size: 14px; -// line-height: 14px; -// color: #646464; } } -// .tags { -// margin-top: 0; } -// .votes, .answers, .favorites, .views { -// text-align: center; -// margin: 0 3px; -// padding: 8px 2px 0px 2px; -// width: 51px; -// float: right; -// height: 44px; -// border: #dbdbd4 1px solid; } -// .votes { -// background: url(../default/media/images/vote-background.png) repeat-x; } -// .answers { -// background: url(../default/media/images/answers-background.png) repeat-x; } -// .views { -// background: url(../default/media/images/view-background.png) repeat-x; } -// .no-votes .item-count { -// color: #b1b5b6; } -// .some-votes .item-count { -// color: #4a757f; } -// .no-answers .item-count { -// color: #b1b5b6; } -// .some-answers .item-count { -// color: #eab243; } -// .no-views .item-count { -// color: #b1b5b6; } -// .some-views .item-count { -// color: #d33f00; } -// .accepted .item-count { -// background: url(../default/media/images/accept.png) no-repeat top right; -// display: block; -// text-align: center; -// width: 40px; -// color: #eab243; } -// .some-favorites .item-count { -// background: #338333; -// color: #d0f5a9; } -// .no-favorites .item-count { -// background: #eab243; -// color: yellow; } } - -// .evenmore { -// font-size: 13px; -// color: #707070; -// padding: 15px 0px 10px 0px; -// clear: both; -// a { -// text-decoration: underline; -// color: #1b79bd; } } - -.pager { - margin-top: 10px; - margin-bottom: 16px; } - -.pagesize { - margin-top: 10px; - margin-bottom: 16px; - float: right; } - -// .paginator { -// padding: 5px 0 10px 0; -// font-size: 13px; -// margin-bottom: 10px; -// .prev a, .next a { -// background-color: #fff; -// color: #777; -// padding: 2px 4px 3px 4px; -// &:visited { -// background-color: #fff; -// color: #777; -// padding: 2px 4px 3px 4px; } } -// a { -// color: #7ea9b3; } -// .prev { -// margin-right: .5em; } -// .next { -// margin-left: .5em; } -// .page a { -// padding: .25em; -// background-color: #fff; -// margin: 0em .25em; -// color: #ff; -// &:visited { -// padding: .25em; -// background-color: #fff; -// margin: 0em .25em; -// color: #ff; } } -// .curr { -// padding: .25em; -// background-color: #fff; -// margin: 0em .25em; -// color: #ff; -// background-color: #8ebcc7; -// color: #fff; -// font-weight: bold; } -// .next a, .prev a { -// color: #7ea9b3; } -// .page a:hover, .curr a:hover, .prev a:hover, .next a:hover { -// color: #8c8c8c; -// background-color: #e1e1e1; -// text-decoration: none; } -// .text { -// color: #777; -// padding: .3em; } -// .paginator-container-left { -// padding: 5px 0 10px 0; } } - -// .tag-size-1 { -// font-size: 12px; } - -// .tag-size-2 { -// font-size: 13px; } - -// .tag-size-3 { -// font-size: 14px; } - -// .tag-size-4 { -// font-size: 15px; } - -// .tag-size-5 { -// font-size: 16px; } - -// .tag-size-6 { -// font-size: 17px; } - -// .tag-size-7 { -// font-size: 18px; } - -// .tag-size-8 { -// font-size: 19px; } - -// .tag-size-9 { -// font-size: 20px; } - -// .tag-size-10 { -// font-size: 21px; } - -// ul { -// &.tags { -// list-style: none; -// margin: 0; -// padding: 0; -// line-height: 170%; -// display: block; -// &.marked-tags { -// list-style: none; -// margin: 0; -// padding: 0; -// line-height: 170%; -// display: block; } } -// &#related-tags { -// list-style: none; -// margin: 0; -// padding: 0; -// line-height: 170%; -// display: block; } -// &.tags li { -// float: left; -// display: block; -// margin: 0 8px 0 0; -// padding: 0; -// height: 20px; } } - -// .wildcard-tags { -// clear: both; } - -// ul.tags.marked-tags li, .wildcard-tags ul.tags li { -// margin-bottom: 5px; } - -// #tagselector div.inputs { -// clear: both; -// float: none; -// margin-bottom: 10px; } - -// .tags-page ul.tags li { -// width: 160px; -// margin: 5px; } - -// ul { -// &#ab-user-tags li { -// width: 160px; -// margin: 5px; } -// &#related-tags li { -// margin: 0 5px 8px 0; -// float: left; -// clear: left; } } - -// .tag-left { -// cursor: pointer; -// display: block; -// float: left; -// height: 17px; -// margin: 0 5px 0 0; -// padding: 0; -// -webkit-box-shadow: 0px 0px 5px #d3d6d7; -// -moz-box-shadow: 0px 0px 5px #d3d6d7; -// box-shadow: 0px 0px 5px #d3d6d7; } - -// .tag-right { -// background: #f3f6f6; -// border: #fff 1px solid; -// border-top: #fff 2px solid; -// outline: #cfdbdb 1px solid; -// display: block; -// float: left; -// height: 17px; -// line-height: 17px; -// font-weight: normal; -// font-size: 11px; -// padding: 0px 8px 0px 8px; -// text-decoration: none; -// text-align: center; -// white-space: nowrap; -// vertical-align: middle; -// font-family: arial; -// color: #717179; } - -// .deletable-tag { -// margin-right: 3px; -// white-space: nowrap; -// border-top-right-radius: 4px; -// border-bottom-right-radius: 4px; -// -moz-border-radius-topright: 4px; -// -moz-border-radius-bottomright: 4px; -// -webkit-border-bottom-right-radius: 4px; -// -webkit-border-top-right-radius: 4px; } - -// .tags { -// a.tag-right, span.tag-right { -// color: #585858; -// text-decoration: none; } -// a:hover { -// color: #1a1a1a; } } - -// .users-page h1, .tags-page h1 { -// float: left; } - -// .main-page h1 { -// margin-right: 5px; } - -// .delete-icon { -// margin-top: -1px; -// float: left; -// height: 21px; -// width: 18px; -// display: block; -// line-height: 20px; -// text-align: center; -// background: #bbcdcd; -// cursor: default; -// color: #fff; -// border-top: #cfdbdb 1px solid; -// font-family: arial; -// border-top-right-radius: 4px; -// border-bottom-right-radius: 4px; -// -moz-border-radius-topright: 4px; -// -moz-border-radius-bottomright: 4px; -// -webkit-border-bottom-right-radius: 4px; -// -webkit-border-top-right-radius: 4px; -// text-shadow: 0px 1px 0px #7ea0a0; -// -moz-text-shadow: 0px 1px 0px #7ea0a0; -// -webkit-text-shadow: 0px 1px 0px #7ea0a0; -// &:hover { -// background: #b32f2f; } } - -// .tag-number { -// font-weight: normal; -// float: left; -// font-size: 16px; -// color: #5d5d5d; } - -// .badges .tag-number { -// float: none; -// display: inline; -// padding-right: 15px; } - -// .section-title { -// color: #7ea9b3; -// font-family: 'yanone kaffeesatz',sans-serif; -// font-weight: bold; -// font-size: 24px; } - -// #fmask { -// margin-bottom: 30px; -// width: 100%; } - -// #askformbar { -// display: inline-block; -// padding: 4px 7px 5px 0px; -// margin-top: 0px; -// p { -// margin: 0 0 5px 0; -// font-size: 14px; -// color: #525252; -// line-height: 1.4; } -// .questiontitleinput { -// font-size: 24px; -// line-height: 24px; -// height: 36px; -// margin: 0px; -// padding: 0px 0 0 5px; -// border: #cce6ec 3px solid; -// width: 725px; } } - -// .ask-page div#question-list, .edit-question-page div#question-list { -// float: none; -// border-bottom: #f0f0ec 1px solid; -// float: left; -// margin-bottom: 10px; } - -// .ask-page div#question-list a, .edit-question-page div#question-list a { -// line-height: 30px; } - -// .ask-page div#question-list h2, .edit-question-page div#question-list h2 { -// font-size: 13px; -// padding-bottom: 0; -// color: #1b79bd; -// border-top: #f0f0ec 1px solid; -// border-left: #f0f0ec 1px solid; -// height: 30px; -// line-height: 30px; -// font-weight: normal; } - -// .ask-page div#question-list span, .edit-question-page div#question-list span { -// width: 28px; -// height: 26px; -// line-height: 26px; -// text-align: center; -// margin-right: 10px; -// float: left; -// display: block; -// color: #fff; -// background: #b8d0d5; -// border-radius: 3px; -// -ms-border-radius: 3px; -// -moz-border-radius: 3px; -// -webkit-border-radius: 3px; -// -khtml-border-radius: 3px; } - -// .ask-page label, .edit-question-page label { -// color: #525252; -// font-size: 13px; } - -// .ask-page #id_tags, .edit-question-page #id_tags { -// border: #cce6ec 3px solid; -// height: 25px; -// padding-left: 5px; -// width: 395px; -// font-size: 14px; } - -// .title-desc { -// color: #707070; -// font-size: 13px; } - -// #fmanswer input.submit, .ask-page input.submit, .edit-question-page input.submit { -// float: left; -// background: url(../default/media/images/medium-button.png) top repeat-x; -// height: 34px; -// border: 0; -// font-family: 'yanone kaffeesatz',sans-serif; -// color: #4a757f; -// font-weight: normal; -// font-size: 21px; -// margin-top: 3px; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// -webkit-box-shadow: 1px 1px 2px #636363; -// -moz-box-shadow: 1px 1px 2px #636363; -// box-shadow: 1px 1px 2px #636363; -// margin-right: 7px; } - -// #fmanswer input.submit:hover, .ask-page input.submit:hover, .edit-question-page input.submit:hover { -// text-decoration: none; -// background: url(../default/media/images/medium-button.png) bottom repeat-x; -// text-shadow: 0px 1px 0px #c6d9dd; -// -moz-text-shadow: 0px 1px 0px #c6d9dd; -// -webkit-text-shadow: 0px 1px 0px #c6d9dd; } - -// #editor { -// font-size: 100%; -// min-height: 200px; -// line-height: 18px; -// margin: 0; -// border-left: #cce6ec 3px solid; -// border-bottom: #cce6ec 3px solid; -// border-right: #cce6ec 3px solid; -// border-top: 0; -// padding: 10px; -// margin-bottom: 10px; -// width: 710px; } - -// #id_title { -// width: 100%; } - -// .wmd-preview { -// margin: 3px 0 5px 0; -// padding: 6px; -// background-color: #f5f5f5; -// min-height: 20px; -// overflow: auto; -// font-size: 13px; -// font-family: arial; -// p { -// margin-bottom: 14px; -// line-height: 1.4; -// font-size: 14px; } -// pre { -// background-color: #e7f1f8; } -// blockquote { -// background-color: #eee; } -// img { -// max-width: 600px; } } - -// .preview-toggle { -// width: 100%; -// color: #b6a475; -// text-align: left; -// span:hover { -// cursor: pointer; } } - -// .after-editor { -// margin-top: 15px; -// margin-bottom: 15px; } - -.checkbox { - margin-left: 5px; - font-weight: normal; - cursor: help; } - -// .question-options { -// margin-top: 1px; -// color: #666; -// line-height: 13px; -// margin-bottom: 5px; -// label { -// vertical-align: text-bottom; } } - -// .edit-content-html { -// border-top: 1px dotted #d8d2a9; -// border-bottom: 1px dotted #d8d2a9; -// margin: 5px 0 5px 0; } - -// .edit-question-page, #fmedit, .wmd-preview { -// color: #525252; } - -// .edit-question-page #id_revision, #fmedit #id_revision, .wmd-preview #id_revision { -// font-size: 14px; -// margin-top: 5px; -// margin-bottom: 5px; } - -// .edit-question-page #id_title, #fmedit #id_title, .wmd-preview #id_title { -// font-size: 24px; -// line-height: 24px; -// height: 36px; -// margin: 0px; -// padding: 0px 0 0 5px; -// border: #cce6ec 3px solid; -// width: 725px; -// margin-bottom: 10px; } - -// .edit-question-page #id_summary, #fmedit #id_summary, .wmd-preview #id_summary { -// border: #cce6ec 3px solid; -// height: 25px; -// padding-left: 5px; -// width: 395px; -// font-size: 14px; } - -// .edit-question-page .title-desc, #fmedit .title-desc, .wmd-preview .title-desc { -// margin-bottom: 10px; } - -// .question-page { -// h1 { -// padding-top: 0px; -// font-family: 'yanone kaffeesatz',sans-serif; -// a { -// color: #464646; -// font-size: 30px; -// font-weight: normal; -// line-height: 1; } } -// p.rss { -// float: none; -// clear: both; -// padding: 3px 0 0 23px; -// font-size: 15px; -// width: 110px; -// background-position: center left; -// margin-left: 0px !important; -// a { -// font-family: 'yanone kaffeesatz',sans-serif; -// vertical-align: top; } } -// .question-content { -// float: right; -// width: 682px; -// margin-bottom: 10px; } -// #question-table { -// float: left; -// border-top: #f0f0f0 1px solid; -// margin: 6px 0 6px 0; -// border-spacing: 0px; -// width: 670px; -// padding-right: 10px; } -// .answer-table { -// margin: 6px 0 6px 0; -// border-spacing: 0px; -// width: 670px; -// padding-right: 10px; -// margin-top: 0px; -// border-bottom: 1px solid #d4d4d4; -// float: right; -// td { -// width: 20px; -// vertical-align: top; } } -// #question-table td { -// width: 20px; -// vertical-align: top; } -// .question-body, .answer-body { -// overflow: auto; -// margin-top: 10px; -// font-family: arial; -// color: #4b4b4b; } -// .question-body p, .answer-body p { -// margin-bottom: 14px; -// line-height: 1.4; -// font-size: 14px; -// padding: 0px 5px 5px 0px; } -// .question-body a, .answer-body a { -// color: #1b79bd; } -// .question-body li, .answer-body li { -// margin-bottom: 7px; } -// .question-body img, .answer-body img { -// max-width: 600px; } -// .post-update-info-container { -// float: right; -// width: 175px; } -// .post-update-info { -// background: white url(../default/media/images/background-user-info.png) repeat-x bottom; -// float: right; -// font-size: 9px; -// font-family: arial; -// width: 158px; -// padding: 4px; -// margin: 0px 0px 5px 5px; -// line-height: 14px; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// -webkit-box-shadow: 0px 2px 1px #bfbfbf; -// -moz-box-shadow: 0px 2px 1px #bfbfbf; -// box-shadow: 0px 2px 1px #bfbfbf; -// p { -// line-height: 13px; -// font-size: 11px; -// margin: 0 0 2px 1px; -// padding: 0; } -// a { -// color: #444; } -// .gravatar { -// float: left; -// margin-right: 4px; } -// p.tip { -// color: #444; -// line-height: 13px; -// font-size: 10px; } } -// .post-controls { -// font-size: 11px; -// line-height: 12px; -// min-width: 200px; -// padding-left: 5px; -// text-align: right; -// clear: left; -// float: right; -// margin-top: 10px; -// margin-bottom: 8px; -// a { -// color: #777; -// padding: 0px 3px 3px 22px; -// cursor: pointer; -// border: none; -// font-size: 12px; -// font-family: arial; -// text-decoration: none; -// height: 18px; -// display: block; -// float: right; -// line-height: 18px; -// margin-top: -2px; -// margin-left: 4px; -// &:hover { -// background-color: #f5f0c9; -// border-radius: 3px; -// -ms-border-radius: 3px; -// -moz-border-radius: 3px; -// -webkit-border-radius: 3px; -// -khtml-border-radius: 3px; } } -// .sep { -// color: #ccc; -// float: right; -// height: 18px; -// font-size: 18px; } -// .question-delete { -// background: url(../default/media/images/delete.png) no-repeat center left; -// padding-left: 16px; } } -// .answer-controls .question-delete { -// background: url(../default/media/images/delete.png) no-repeat center left; -// padding-left: 16px; } -// .post-controls .question-flag, .answer-controls .question-flag { -// background: url(../default/media/images/flag.png) no-repeat center left; } -// .post-controls .question-edit, .answer-controls .question-edit { -// background: url(../default/media/images/edit2.png) no-repeat center left; } -// .post-controls .question-retag, .answer-controls .question-retag { -// background: url(../default/media/images/retag.png) no-repeat center left; } -// .post-controls .question-close, .answer-controls .question-close { -// background: url(../default/media/images/close.png) no-repeat center left; } -// .post-controls .permant-link, .answer-controls .permant-link { -// background: url(../default/media/images/link.png) no-repeat center left; } -// .tabbar { -// width: 100%; } -// #questioncount { -// float: left; -// font-family: 'yanone kaffeesatz',sans-serif; -// line-height: 15px; } -// .question-img-upvote, .question-img-downvote, .answer-img-upvote, .answer-img-downvote { -// width: 25px; -// height: 20px; -// cursor: pointer; } -// .question-img-upvote, .answer-img-upvote { -// background: url(../default/media/images/vote-arrow-up-new.png) no-repeat; } -// .question-img-downvote, .answer-img-downvote { -// background: url(../default/media/images/vote-arrow-down-new.png) no-repeat; } -// .question-img-upvote { -// &:hover, &.on { -// background: url(../default/media/images/vote-arrow-up-on-new.png) no-repeat; } } -// .answer-img-upvote { -// &:hover, &.on { -// background: url(../default/media/images/vote-arrow-up-on-new.png) no-repeat; } } -// .question-img-downvote { -// &:hover, &.on { -// background: url(../default/media/images/vote-arrow-down-on-new.png) no-repeat; } } -// .answer-img-downvote { -// &:hover, &.on { -// background: url(../default/media/images/vote-arrow-down-on-new.png) no-repeat; } } -// #fmanswer_button { -// margin: 8px 0px; } -// .question-img-favorite:hover { -// background: url(../default/media/images/vote-favorite-on.png); } -// div.comments { -// padding: 0; } -// #comment-title { -// font-weight: bold; -// font-size: 23px; -// color: #7ea9b3; -// width: 200px; -// float: left; -// font-family: 'yanone kaffeesatz',sans-serif; } -// .comments { -// font-size: 12px; -// clear: both; -// div.controls { -// clear: both; -// float: left; -// width: 100%; -// margin: 3px 0 20px 5px; } -// .controls a { -// color: #988e4c; -// padding: 0 3px 2px 22px; -// font-family: arial; -// font-size: 13px; -// background: url(../default/media/images/comment.png) no-repeat center left; -// &:hover { -// background-color: #f5f0c9; -// text-decoration: none; } } -// .button { -// color: #988e4c; -// font-size: 11px; -// padding: 3px; -// cursor: pointer; } -// a { -// background-color: inherit; -// color: #1b79bd; -// padding: 0; } -// form.post-comments { -// margin: 3px 26px 0 42px; -// textarea { -// font-size: 13px; -// line-height: 1.3; } } -// textarea { -// height: 42px; -// width: 100%; -// margin: 7px 0 5px 1px; -// font-family: arial; -// outline: none; -// overflow: auto; -// font-size: 12px; -// line-height: 140%; -// padding-left: 2px; -// padding-top: 3px; -// border: #cce6ec 3px solid; } -// input { -// margin-left: 10px; -// margin-top: 1px; -// vertical-align: top; -// width: 100px; } -// button { -// background: url(../default/media/images/small-button-blue.png) repeat-x top; -// border: 0; -// color: #4a757f; -// font-family: arial; -// font-size: 13px; -// width: 100px; -// font-weight: bold; -// height: 27px; -// line-height: 25px; -// margin-bottom: 5px; -// cursor: pointer; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// text-shadow: 0px 1px 0px #e6f6fa; -// -moz-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-box-shadow: 1px 1px 2px #808080; -// -moz-box-shadow: 1px 1px 2px #808080; -// box-shadow: 1px 1px 2px #808080; -// &:hover { -// background: url(../default/media/images/small-button-blue.png) bottom repeat-x; -// text-shadow: 0px 1px 0px #c6d9dd; -// -moz-text-shadow: 0px 1px 0px #c6d9dd; -// -webkit-text-shadow: 0px 1px 0px #c6d9dd; } } -// .counter { -// display: inline-block; -// width: 245px; -// float: right; -// color: #b6a475 !important; -// vertical-align: top; -// font-family: arial; -// float: right; -// text-align: right; } -// .comment { -// border-bottom: 1px solid #edeeeb; -// clear: both; -// margin: 0; -// margin-top: 8px; -// padding-bottom: 4px; -// overflow: auto; -// font-family: arial; -// font-size: 11px; -// min-height: 25px; -// background: white url(../default/media/images/comment-background.png) bottom repeat-x; -// border-radius: 5px; -// -ms-border-radius: 5px; -// -moz-border-radius: 5px; -// -webkit-border-radius: 5px; -// -khtml-border-radius: 5px; } -// div.comment:hover { -// background-color: #efefef; } -// a.author { -// background-color: inherit; -// color: #1b79bd; -// padding: 0; -// &:hover { -// text-decoration: underline; } } -// span.delete-icon { -// background: url(../default/media/images/close-small.png) no-repeat; -// border: 0; -// width: 14px; -// height: 14px; -// &:hover { -// border: #bc564b 2px solid; -// border-radius: 10px; -// -ms-border-radius: 10px; -// -moz-border-radius: 10px; -// -webkit-border-radius: 10px; -// -khtml-border-radius: 10px; -// margin: -3px 0px 0px -2px; } } -// .content { -// margin-bottom: 7px; } -// .comment-votes { -// float: left; -// width: 37px; -// line-height: 130%; -// padding: 6px 5px 6px 3px; } -// .comment-body { -// line-height: 1.3; -// margin: 3px 26px 0 46px; -// padding: 5px 3px; -// color: #666; -// font-size: 13px; -// .edit { -// padding-left: 6px; } -// p { -// font-size: 13px; -// line-height: 1.3; -// margin-bottom: 3px; -// padding: 0; } } -// .comment-delete { -// float: right; -// width: 14px; -// line-height: 130%; -// padding: 8px 6px; } -// .upvote { -// margin: 0px; -// padding-right: 17px; -// padding-top: 2px; -// text-align: right; -// height: 20px; -// font-size: 13px; -// font-weight: bold; -// color: #777; -// &.upvoted { -// color: #d64000; } -// &.hover { -// background: url(../default/media/images/go-up-grey.png) no-repeat; -// background-position: right 1px; } -// &:hover { -// background: url(../default/media/images/go-up-orange.png) no-repeat; -// background-position: right 1px; } } -// .help-text { -// float: right; -// text-align: right; -// color: gray; -// margin-bottom: 0px; -// margin-top: 0px; -// line-height: 50%; } } -// #questiontools { -// font-size: 22px; -// margin-top: 11px; -// text-align: left; } -// .question-status { -// margin-top: 10px; -// margin-bottom: 15px; -// padding: 20px; -// background-color: #fef7cc; -// text-align: center; -// border: #e1c04a 1px solid; -// h3 { -// font-size: 20px; -// color: #707070; -// font-weight: normal; } } -// .vote-buttons { -// // float: left; -// // text-align: center; -// // padding-top: 2px; -// // margin: 10px 10px 0px 3px; -// img { -// cursor: pointer; } } -// .vote-number { -// font-family: 'yanone kaffeesatz',sans-serif; -// padding: 0px 0 5px 0; -// font-size: 25px; -// font-weight: bold; -// color: #777; } -// .vote-buttons .notify-sidebar { -// // text-align: left; -// // width: 120px; -// label { -// vertical-align: top; } } -// .tabbar-answer { -// margin-bottom: 15px; -// padding-left: 7px; -// width: 723px; -// margin-top: 10px; } -// .answer .vote-buttons { -// // float: left; -// } -// .accepted-answer { -// background-color: #f7fecc; -// border-bottom-color: #9bd59b; -// // .vote-buttons { -// // width: 27px; -// // margin-right: 10px; -// // margin-top: 10px; } -// } -// .answer .post-update-info a { -// color: #444444; } -// .answered { -// background: #ccc; -// color: #999; } -// .answered-accepted { -// background: #dcdcdc; -// color: #763333; -// strong { -// color: #e1e818; } } -// .answered-by-owner { -// background: #f1f1ff; -// .comments { -// .button { -// background-color: #e6ecff; } -// background-color: #e6ecff; } -// // .vote-buttons { -// // margin-right: 10px; } -// } -// .answer-img-accept:hover { -// background: url(../default/media/images/vote-accepted-on.png); } -// .answer-body { -// a { -// color: #1b79bd; } -// li { -// margin-bottom: 0.7em; } } -// #fmanswer { -// color: #707070; -// line-height: 1.2; -// margin-top: 10px; -// h2 { -// font-family: 'yanone kaffeesatz',sans-serif; -// color: #7ea9b3; -// font-size: 24px; } -// label { -// font-size: 13px; } } -// .message { -// padding: 5px; -// margin: 0px 0 10px 0; } } - -// @media screen and (-webkit-min-device-pixel-ratio:0) { -// textarea { -// padding-left: 3px !important; } } - -// .facebook-share.icon, .twitter-share.icon, .linkedin-share.icon, .identica-share.icon { -// background: url(../default/media/images/socialsprite.png) no-repeat; -// display: block; -// text-indent: -100em; -// height: 25px; -// width: 25px; -// margin-bottom: 3px; } - -// .facebook-share.icon:hover, .twitter-share.icon:hover, .linkedin-share.icon:hover, .identica-share.icon:hover { -// opacity: 0.8; -// filter: alpha(opacity = 80); } - -// .facebook-share.icon { -// background-position: -26px 0px; } - -// .identica-share.icon { -// background-position: -78px 0px; } - -// .twitter-share.icon { -// margin-top: 10px; -// background-position: 0px 0px; } - -// .linkedin-share.icon { -// background-position: -52px 0px; } - -// .openid-signin, .meta, .users-page, .user-profile-edit-page { -// font-size: 13px; -// line-height: 1.3; -// color: #525252; } - -// .openid-signin p, .meta p, .users-page p, .user-profile-edit-page p { -// font-size: 13px; -// color: #707070; -// line-height: 1.3; -// font-family: arial; -// color: #525252; -// margin-bottom: 12px; } - -// .openid-signin h2, .meta h2, .users-page h2, .user-profile-edit-page h2 { -// color: #525252; -// padding-left: 0px; -// font-size: 16px; } - -// .openid-signin form, .meta form, .users-page form, .user-profile-edit-page form, .user-profile-page form { -// margin-bottom: 15px; } - -// .openid-signin input[type="text"], .meta input[type="text"], .users-page input[type="text"], .user-profile-edit-page input[type="text"], .user-profile-page input[type="text"], .openid-signin input[type="password"], .meta input[type="password"], .users-page input[type="password"], .user-profile-edit-page input[type="password"], .user-profile-page input[type="password"], .openid-signin select, .meta select, .users-page select, .user-profile-edit-page select, .user-profile-page select { -// border: #cce6ec 3px solid; -// height: 25px; -// padding-left: 5px; -// width: 395px; -// font-size: 14px; } - -// .openid-signin select, .meta select, .users-page select, .user-profile-edit-page select, .user-profile-page select { -// width: 405px; -// height: 30px; } - -// .openid-signin textarea, .meta textarea, .users-page textarea, .user-profile-edit-page textarea, .user-profile-page textarea { -// border: #cce6ec 3px solid; -// padding-left: 5px; -// padding-top: 5px; -// width: 395px; -// font-size: 14px; } - -// .openid-signin input.submit, .meta input.submit, .users-page input.submit, .user-profile-edit-page input.submit, .user-profile-page input.submit { -// background: url(../default/media/images/small-button-blue.png) repeat-x top; -// border: 0; -// color: #4a757f; -// font-weight: bold; -// font-size: 13px; -// font-family: arial; -// height: 26px; -// margin: 5px 0px; -// width: 100px; -// cursor: pointer; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// text-shadow: 0px 1px 0px #e6f6fa; -// -moz-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-box-shadow: 1px 1px 2px #808080; -// -moz-box-shadow: 1px 1px 2px #808080; -// box-shadow: 1px 1px 2px #808080; } - -// .openid-signin input.submit:hover, .meta input.submit:hover, .users-page input.submit:hover, .user-profile-edit-page input.submit:hover, .user-profile-page input.submit:hover { -// background: url(../default/media/images/small-button-blue.png) repeat-x bottom; -// text-decoration: none; } - -.openid-signin .cancel, .meta .cancel, .users-page .cancel, .user-profile-edit-page .cancel, .user-profile-page .cancel { - background: url(../default/media/images/small-button-cancel.png) repeat-x top !important; - color: #525252 !important; } - -.openid-signin .cancel:hover, .meta .cancel:hover, .users-page .cancel:hover, .user-profile-edit-page .cancel:hover, .user-profile-page .cancel:hover { - background: url(../default/media/images/small-button-cancel.png) repeat-x bottom !important; } - -#email-input-fs, #local_login_buttons, #password-fs, #openid-fs { - margin-top: 10px; } - -#email-input-fs #id_email, #local_login_buttons #id_email, #password-fs #id_email, #openid-fs #id_email, #email-input-fs #id_username, #local_login_buttons #id_username, #password-fs #id_username, #openid-fs #id_username, #email-input-fs #id_password, #local_login_buttons #id_password, #password-fs #id_password, #openid-fs #id_password { - font-size: 12px; - line-height: 20px; - height: 20px; - margin: 0px; - padding: 0px 0 0 5px; - border: #cce6ec 3px solid; - width: 200px; } - -#email-input-fs .submit-b, #local_login_buttons .submit-b, #password-fs .submit-b, #openid-fs .submit-b { - background: url(../default/media/images/small-button-blue.png) repeat-x top; - border: 0; - color: #4a757f; - font-weight: bold; - font-size: 13px; - font-family: arial; - height: 24px; - margin-top: -2px; - padding-left: 10px; - padding-right: 10px; - cursor: pointer; - border-radius: 4px; - -ms-border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; } - -#email-input-fs .submit-b:hover, #local_login_buttons .submit-b:hover, #password-fs .submit-b:hover, #openid-fs .submit-b:hover { - background: url(../default/media/images/small-button-blue.png) repeat-x bottom; } - -.openid-input { - background: url(../default/media/images/openid.gif) no-repeat; - padding-left: 15px; - cursor: pointer; } - -.openid-login-input { - background-position: center left; - background: url(../default/media/images/openid.gif) no-repeat 0% 50%; - padding: 5px 5px 5px 15px; - cursor: pointer; - font-family: trebuchet ms; - font-weight: 300; - font-size: 150%; - width: 500px; } - -.openid-login-submit { - height: 40px; - width: 80px; - line-height: 40px; - cursor: pointer; - border: 1px solid #777; - font-weight: bold; - font-size: 120%; } - -.tabbar-user { - width: 375px; } - -// .user { -// padding: 5px; -// line-height: 140%; -// width: 166px; -// border: #eee 1px solid; -// margin-bottom: 5px; -// border-radius: 3px; -// -ms-border-radius: 3px; -// -moz-border-radius: 3px; -// -webkit-border-radius: 3px; -// -khtml-border-radius: 3px; -// .user-micro-info { -// color: #525252; } -// ul { -// margin: 0; -// list-style-type: none; } -// .thumb { -// clear: both; -// float: left; -// margin-right: 4px; -// display: inline; } } - -// .tabbar-tags { -// width: 270px; -// margin-bottom: 15px; } - -// a { -// &.medal { -// font-size: 17px; -// line-height: 250%; -// margin-right: 5px; -// color: #333; -// text-decoration: none; -// background: url(../default/media/images/medala.gif) no-repeat; -// border-left: 1px solid #eee; -// border-top: 1px solid #eee; -// border-bottom: 1px solid #ccc; -// border-right: 1px solid #ccc; -// padding: 4px 12px 4px 6px; } -// &:hover.medal { -// color: #333; -// text-decoration: none; -// background: url(../default/media/images/medala_on.gif) no-repeat; -// border-left: 1px solid #e7e296; -// border-top: 1px solid #e7e296; -// border-bottom: 1px solid #d1ca3d; -// border-right: 1px solid #d1ca3d; } } - -#award-list .user { - float: left; - margin: 5px; } - -.tabbar-profile { - width: 100%; - margin-bottom: 15px; - float: left; } - -// .user-profile-page { -// font-size: 13px; -// color: #525252; -// p { -// font-size: 13px; -// line-height: 1.3; -// color: #525252; } -// .avatar img { -// border: #eee 1px solid; -// padding: 5px; } -// h2 { -// padding: 10px 0px 10px 0px; -// font-family: 'yanone kaffeesatz',sans-serif; } } - -.user-details { - font-size: 13px; - h3 { - font-size: 16px; } } - -.user-about { - background-color: #eeeeee; - height: 200px; - line-height: 20px; - overflow: auto; - padding: 10px; - width: 90%; - p { - font-size: 13px; } } - -// .follow-toggle, .submit { -// border: 0 !important; -// color: #4a757f; -// font-weight: bold; -// font-size: 12px; -// height: 26px; -// line-height: 26px; -// margin-top: -2px; -// font-size: 15px; -// cursor: pointer; -// font-family: 'yanone kaffeesatz',sans-serif; -// background: url(../default/media/images/small-button-blue.png) repeat-x top; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// text-shadow: 0px 1px 0px #e6f6fa; -// -moz-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-text-shadow: 0px 1px 0px #e6f6fa; -// -webkit-box-shadow: 1px 1px 2px #808080; -// -moz-box-shadow: 1px 1px 2px #808080; -// box-shadow: 1px 1px 2px #808080; } - -// .follow-toggle:hover, .submit:hover { -// background: url(../default/media/images/small-button-blue.png) repeat-x bottom; -// text-decoration: none !important; } - -// .follow-toggle { -// .follow { -// font-color: #000; -// font-style: normal; } -// .unfollow { -// div.unfollow-red { -// display: none; } -// &:hover div { -// &.unfollow-red { -// display: inline; -// color: #fff; -// font-weight: bold; -// color: #a05736; } -// &.unfollow-green { -// display: none; } } } } - -.count { - font-family: 'yanone kaffeesatz',sans-serif; - font-size: 200%; - font-weight: 700; - color: #777777; } - -.scorenumber { - font-family: 'yanone kaffeesatz',sans-serif; - font-size: 35px; - font-weight: 800; - color: #777; - line-height: 40px; - margin-top: 3px; } - -.vote-count { - font-family: arial; - font-size: 160%; - font-weight: 700; - color: #777; } - -// .answer-summary { -// display: block; -// clear: both; -// padding: 3px; } - -.answer-votes { - background-color: #eeeeee; - color: #555555; - float: left; - font-family: arial; - font-size: 15px; - font-weight: bold; - height: 17px; - padding: 2px 4px 5px; - text-align: center; - text-decoration: none; - width: 20px; - margin-right: 10px; - border-radius: 4px; - -ms-border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - -khtml-border-radius: 4px; } - -.karma-summary { - padding: 5px; - font-size: 13px; - h3 { - text-align: center; - font-weight: bold; - padding: 5px; } } - -.karma-diagram { - width: 477px; - height: 300px; - float: left; - margin-right: 10px; } - -.karma-details { - float: right; - width: 450px; - height: 250px; - overflow-y: auto; - word-wrap: break-word; - p { - margin-bottom: 10px; } } - -.karma-gained { - font-weight: bold; - background: #eee; - width: 25px; - margin-right: 5px; - color: green; - padding: 3px; - display: block; - float: left; - text-align: center; - border-radius: 3px; - -ms-border-radius: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - -khtml-border-radius: 3px; } - -.karma-lost { - font-weight: bold; - background: #eee; - width: 25px; - color: red; - padding: 3px; - display: block; - margin-right: 5px; - float: left; - text-align: center; - border-radius: 3px; - -ms-border-radius: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - -khtml-border-radius: 3px; } - -.submit-row { - margin-bottom: 10px; } - -.revision { - margin: 10px 0 10px 0; - font-size: 13px; - color: #525252; - p { - font-size: 13px; - line-height: 1.3; - color: #525252; } - h3 { - font-family: 'yanone kaffeesatz',sans-serif; - font-size: 21px; - padding-left: 0px; } - .header { - background-color: #f5f5f5; - padding: 5px; - cursor: pointer; } - .author { - background-color: #e9f3f5; } - .summary { - padding: 5px 0 10px 0; - span { - background-color: #fde785; - padding: 6px; - border-radius: 4px; - -ms-border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - -khtml-border-radius: 4px; - display: inline; - -webkit-box-shadow: 1px 1px 4px #cfb852; - -moz-box-shadow: 1px 1px 4px #cfb852; - box-shadow: 1px 1px 4px #cfb852; } } - .answerbody { - padding: 10px 0 5px 10px; } - .revision-mark { - width: 150px; - text-align: left; - display: inline-block; - font-size: 11px; - overflow: hidden; - .gravatar { - float: left; - margin-right: 4px; - padding-top: 5px; } } - .revision-number { - font-size: 300%; - font-weight: bold; - font-family: sans-serif; } } - -// del { -// color: #c34719; -// .post-tag { -// color: #c34719; } } - -ins { - .post-tag, p { - background-color: #e6f0a2; } - background-color: #e6f0a2; } - -// .vote-notification { -// z-index: 1; -// cursor: pointer; -// display: none; -// position: absolute; -// font-family: arial; -// font-size: 14px; -// font-weight: normal; -// color: white; -// background-color: #8e0000; -// text-align: center; -// padding-bottom: 10px; -// -webkit-box-shadow: 0px 2px 4px #370000; -// -moz-box-shadow: 0px 2px 4px #370000; -// box-shadow: 0px 2px 4px #370000; -// border-radius: 4px; -// -ms-border-radius: 4px; -// -moz-border-radius: 4px; -// -webkit-border-radius: 4px; -// -khtml-border-radius: 4px; -// h3 { -// background: url(../default/media/images/notification.png) repeat-x top; -// padding: 10px 10px 10px 10px; -// font-size: 13px; -// margin-bottom: 5px; -// border-top: #8e0000 1px solid; -// color: #fff; -// font-weight: normal; -// border-top-right-radius: 4px; -// border-top-left-radius: 4px; -// -moz-border-radius-topright: 4px; -// -moz-border-radius-topleft: 4px; -// -webkit-border-top-left-radius: 4px; -// -webkit-border-top-right-radius: 4px; } -// a { -// color: #fb7321; -// text-decoration: underline; -// font-weight: bold; } } - -// #ground { -// width: 100%; -// clear: both; -// border-top: 1px solid #000; -// padding: 6px 0 0 0; -// background: #16160f; -// font-size: 16px; -// font-family: 'yanone kaffeesatz',sans-serif; -// p { -// margin-bottom: 0; } } - -.footer-links { - color: #eee; - text-align: left; - width: 500px; - float: left; - a { - color: #e7e8a8; } } - -.powered-link { - width: 500px; - float: left; - text-align: left; - a { - color: #8ebcc7; } } - -.copyright { - color: #616161; - width: 450px; - float: right; - text-align: right; - a { - color: #8ebcc7; } - img.license-logo { - margin: 6px 0px 20px 10px; - float: right; } } - -.notify-me { - float: left; } - -span { - &.text-counter { - margin-right: 20px; } - // &.form-error { - // color: #990000; - // font-weight: normal; - // margin-left: 5px; } - } - -p.form-item { - margin: 0px; } - -// .deleted { -// background: #f4e7e7 none repeat scroll 0 0; } - -.form-row { - line-height: 25px; } - -table { - &.form-as-table { - margin-top: 5px; - ul { - list-style-type: none; - display: inline; } - li { - display: inline; } - td { - text-align: right; } - th { - text-align: left; - font-weight: normal; } } - &.ab-subscr-form, &.ab-tag-filter-form { - width: 45em; } } - -.submit-row { - line-height: 30px; - padding-top: 10px; - display: block; - clear: both; } - -.errors { - line-height: 20px; - color: red; } - -.error { - color: darkred; - margin: 0; - font-size: 10px; } - -label.retag-error { - color: darkred; - padding-left: 5px; - font-size: 10px; } - -.fieldset { - border: none; - margin-top: 10px; - padding: 10px; } - -// span.form-error { -// color: #990000; -// font-size: 90%; -// font-weight: normal; -// margin-left: 5px; } - -.favorites-empty { - width: 32px; - height: 45px; - float: left; } - -.user-info-table { - margin-bottom: 10px; - border-spacing: 0; } - -.user-stats-table .narrow { - width: 660px; } - -.narrow .summary h3 { - padding: 0px; - margin: 0px; } - -.relativetime { - font-weight: bold; - text-decoration: none; } - -// .narrow .tags { -// float: left; } - -.user-action-1 { - font-weight: bold; - color: #333; } - -.user-action-2 { - font-weight: bold; - color: #ccc; } - -.user-action-3, .user-action-4 { - color: #333; } - -.user-action-5, .user-action-6 { - color: darkred; } - -.user-action-7 { - color: #333; } - -.user-action-8 { - padding: 3px; - font-weight: bold; - background-color: #ccc; - color: #763333; } - -.revision-summary { - background-color: #fffe9b; - padding: 2px; } - -.question-title-link a { - font-weight: bold; - color: #0077cc; } - -.answer-title-link a { - color: #333; } - -.post-type-1 a, .post-type-3 a, .post-type-5 a { - font-weight: bold; } - -.post-type-2 a, .post-type-4 a, .post-type-6 a, .post-type-8 a { - color: #333; } - -.hilite, .hilite1 { - background-color: #ff0; } - -.hilite2 { - background-color: #f0f; } - -.hilite3 { - background-color: #0ff; } - -// .gold, .badge1 { -// color: #ffcc00; } - -// .silver, .badge2 { -// color: #cccccc; } - -// .bronze, .badge3 { -// color: #cc9933; } - -.score { - font-weight: 800; - color: #333; } - -a { - &.comment { - background: #eee; - color: #993300; - padding: 5px; } - &.offensive { - color: #999; } } - -.message { - h1 { - padding-top: 0px; - font-size: 15px; } - p { - margin-bottom: 0px; } } - -p.space-above { - margin-top: 10px; } - -.warning { - color: red; } - -button::-moz-focus-inner { - padding: 0; - border: none; } - -// .submit { -// cursor: pointer; -// background-color: #d4d0c8; -// height: 30px; -// border: 1px solid #777777; -// font-weight: bold; -// font-size: 120%; -// &:hover { -// text-decoration: underline; } -// &.small { -// margin-right: 5px; -// height: 20px; -// font-weight: normal; -// font-size: 12px; -// padding: 1px 5px; -// &:hover { -// text-decoration: none; } } } - -.question-page a.submit { - display: -moz-inline-stack; - display: inline-block; - line-height: 30px; - padding: 0 5px; - *display: inline; } - -.noscript { - position: fixed; - top: 0px; - left: 0px; - width: 100%; - z-index: 100; - padding: 5px 0; - text-align: center; - font-family: sans-serif; - font-size: 120%; - font-weight: bold; - color: #ffffff; - background-color: #ae0000; } - -.big { - font-size: 14px; } - -.strong { - font-weight: bold; } - -.orange { - color: #d64000; - font-weight: bold; } - -.grey { - color: #808080; } - -.about div { - padding: 10px 5px 10px 5px; - border-top: 1px dashed #aaaaaa; } - -.highlight { - background-color: #fff8c6; } - -.nomargin { - margin: 0; } - -.margin-bottom { - margin-bottom: 10px; } - -.margin-top { - margin-top: 10px; } - -.inline-block { - display: inline-block; } - -.action-status { - margin: 0; - border: none; - text-align: center; - line-height: 10px; - font-size: 12px; - padding: 0; - span { - padding: 3px 5px 3px 5px; - background-color: #fff380; - font-weight: normal; - -moz-border-radius: 5px; - -khtml-border-radius: 5px; - -webkit-border-radius: 5px; } } - -.list-table td { - vertical-align: top; } - -table.form-as-table { - .errorlist { - display: block; - margin: 0; - padding: 0 0 0 5px; - text-align: left; - font-size: 10px; - color: darkred; } - input { - display: inline; - margin-left: 4px; } - th { - vertical-align: bottom; - padding-bottom: 4px; } } - -.form-row-vertical { - margin-top: 8px; - display: block; - label { - margin-bottom: 3px; - display: block; } } - -.text-align-right { - text-align: center; } - -ul.form-horizontal-rows { - list-style: none; - margin: 0; - li { - position: relative; - height: 40px; } - label { - display: inline-block; } - ul.errorlist { - list-style: none; - color: darkred; - font-size: 10px; - line-height: 10px; - position: absolute; - top: 2px; - left: 180px; - text-align: left; - margin: 0; - li { - height: 10px; } } - label { - position: absolute; - left: 0px; - bottom: 6px; - margin: 0px; - line-height: 12px; - font-size: 12px; } - li input { - position: absolute; - bottom: 0px; - left: 180px; - margin: 0px; } } - -.narrow .summary { - float: left; } - -.user-profile-tool-links { - font-weight: bold; - vertical-align: top; } - -// ul { -// &.post-tags { -// margin-left: 3px; -// li { -// margin-top: 4px; -// margin-bottom: 3px; } } -// &.post-retag { -// margin-bottom: 0px; -// margin-left: 5px; } } - -// #question-controls .tags { -// margin: 0 0 3px 0; } - -// #tagselector { -// padding-bottom: 2px; -// margin-bottom: 0; } - -// #related-tags { -// padding-left: 3px; } - -#hideignoredtagscontrol { - margin: 5px 0 0 0; - label { - font-size: 12px; - color: #666; } } - -#hideignoredtagscb { - margin: 0 2px 0 1px; } - -#recaptcha_widget_div { - width: 318px; - float: left; - clear: both; } - -p.signup_p { - margin: 20px 0px 0px 0px; } - -.simple-subscribe-options ul { - list-style: none; - list-style-position: outside; - margin: 0; } - -.wmd-preview { - a { - color: #1b79bd; } - li { - margin-bottom: 7px; - font-size: 14px; } } - -// .search-result-summary { -// font-weight: bold; -// font-size: 18px; -// line-height: 22px; -// margin: 0px 0px 0px 0px; -// padding: 2px 0 0 0; -// float: left; } - -.faq-rep-item { - text-align: right; - padding-right: 5px; } - -.user-info-table .gravatar { - margin: 0; } - -#responses { - clear: both; - line-height: 18px; - margin-bottom: 15px; - div.face { - float: left; - text-align: center; - width: 54px; - padding: 3px; - overflow: hidden; } } - -.response-parent { - margin-top: 18px; - strong { - font-size: 20px; } } - -.re { - min-height: 57px; - clear: both; - margin-top: 10px; } - -#responses input { - float: left; } - -#re_tools { - margin-bottom: 10px; } - -#re_sections { - margin-bottom: 6px; - .on { - font-weight: bold; } } - -.avatar-page { - ul { - list-style: none; } - li { - display: inline; } } - -// .user-profile-page { -// .avatar p { -// margin-bottom: 0px; } -// .tabbar a#stats { -// margin-left: 0; } -// img.gravatar { -// margin: 2px 0 3px 0; } -// h3 { -// padding: 0; -// margin-top: -3px; } } - -.userlist { - font-size: 13px; } - -img.flag { - border: 1px solid #eee; - vertical-align: text-top; } - -.main-page img.flag { - vertical-align: text-bottom; } - -a.edit { - padding-left: 3px; - color: #145bff; } - -.str { - color: #080; } - -.kwd { - color: #008; } - -.com { - color: #800; } - -.typ { - color: #606; } - -.lit { - color: #066; } - -.pun { - color: #660; } - -.pln { - color: #000; } - -// .tag { -// color: #008; } - -.atn { - color: #606; } - -.atv { - color: #080; } - -.dec { - color: #606; } - -pre.prettyprint { - clear: both; - padding: 3px; - border: 0px solid #888; } - -// @media print { -// .str { -// color: #060; } -// .kwd { -// color: #006; -// font-weight: bold; } -// .com { -// color: #600; -// font-style: italic; } -// .typ { -// color: #404; -// font-weight: bold; } -// .lit { -// color: #044; } -// .pun { -// color: #440; } -// .pln { -// color: #000; } -// .tag { -// color: #006; -// font-weight: bold; } -// .atn { -// color: #404; } -// .atv { -// color: #060; } } diff --git a/lms/static/sass/course/discussion/_badges.scss b/lms/static/sass/course/discussion/_badges.scss deleted file mode 100644 index 65d8cbf513..0000000000 --- a/lms/static/sass/course/discussion/_badges.scss +++ /dev/null @@ -1,88 +0,0 @@ -// Style for the user badge list (can be accessed by clicking "View all MIT badges" in the badge section of the Askbot user profile - -div.badges-intro { - margin: 20px 0; -} - -div.badge-intro { - @extend .badges-intro; - - .badge1, .badge2, .badge3 { - font-size: 20px; - } - - -} - -div#award-list{ - li.username { - font-size: 20px; - margin-bottom: 8px; - } -} - -ul.badge-list { - padding-left: 0; - - li.badge { - border-bottom: 1px solid #eee; - @extend .clearfix; - list-style: none; - padding: 10px 0; - - &:last-child { - border-bottom: 0; - } - - div.check { - float:right; - min-width:flex-grid(1,9); - text-align:right; - - span { - font-size:19px; - padding-right:5px; - color:green; - } - } - div.badge-name { - float:left; - width:flex-grid(3,9); - - span { - font-size: 20px; - } - } - - p { - margin: 0; - float:left; - } - } -} - -.gold, .badge1 { - color: #ffcc00; -} - -.silver, .badge2 { - color: #cccccc; -} - -.bronze, .badge3 { - color: #cc9933; -} - -div.discussion-wrapper aside { - div.badge-desc { - border-top: 0; - - > div { - margin-bottom: 20px; - span { - font-size: 18px; - @include border-radius(10px); - } - } - } -} diff --git a/lms/static/sass/course/discussion/_discussion.scss b/lms/static/sass/course/discussion/_discussion.scss deleted file mode 100644 index 9383241980..0000000000 --- a/lms/static/sass/course/discussion/_discussion.scss +++ /dev/null @@ -1,82 +0,0 @@ -// Generic layout styles for the discussion forums -body.askbot { - section.container { - div.discussion-wrapper { - @extend .table-wrapper; - display: table; - - div.discussion-content { - @include box-sizing(border-box); - display: table-cell; - min-width: 650px; - padding: 40px; - width: flex-grid(9) + flex-gutter(); - - - a.tabula-rasa, .tabula-rasa{ - @extend .light-button; - @include border-radius(5px); - display: block; - margin: 10px auto; - padding: 20px; - text-align: center; - width: flex-grid(5); - text-decoration: none; - color: #888; - font-weight: bold; - - &:first-child { - margin-top: 70px; - } - - &:last-child { - margin-bottom: 70px; - } - - } - } - } - } - -} - -// Autocomplete -.acInput { - width: 200px; -} -.acResults { - background-color: #fff; - border: 1px solid #ababab; - overflow: hidden; - padding: 0px; - @include box-shadow(0 2px 2px #bbb); - - ul { - list-style-position: outside; - list-style: none; - margin: 0; - padding: 0; - width: 100%; - } - - li { - cursor: pointer; - display: block; - font: menu; - margin: 0px; - overflow: hidden; - padding: 5px 10px; - text-align: left; - border-top: 1px solid #eee; - width: 100%; - } -} - -.acLoading { - background : url('../default/media/images/indicator.gif') right center no-repeat; -} - -.acSelect { - background-color: $pink; - color: #fff; -} diff --git a/lms/static/sass/course/discussion/_forms.scss b/lms/static/sass/course/discussion/_forms.scss deleted file mode 100644 index 2f3bd86962..0000000000 --- a/lms/static/sass/course/discussion/_forms.scss +++ /dev/null @@ -1,184 +0,0 @@ -// Styles for different forms in the system - -form.answer-form { - @include box-sizing(border-box); - border-top: 1px solid #ddd; - overflow: hidden; - padding-left: flex-grid(1.1); - padding-top: lh(); - - p { - margin-bottom: lh(); - } - - textarea { - @include box-sizing(border-box); - margin-top: 15px; - resize: vertical; - width: 99%; - - &#editor { - min-height: em(120); - } - } - - div.checkbox { - margin-bottom: lh(); - - label { - display: inline; - } - } - - div.form-item { - margin: 15px 0; - - label { - display: block; - margin-bottom: -5px; - } - - .title-desc { - @include box-sizing(border-box); - @include border-radius(4px); - background: #333; - color: #fff; - display: none; - font-size: 13px; - padding: 7px 14px; - -webkit-font-smoothing: antialiased; - } - - &:hover { - .title-desc { - display: inline-block; - position: absolute; - margin-left: 10px; - z-index: 1; - width: 200px; - - &:before { - border-color: transparent #333 transparent transparent; - border-style:solid; - border-width:12px 12px 12px 0; - content:""; - height:0; - left:-10px; - position:absolute; - top:1; - width:0; - - } - } - } - - } - - span.form-error, label.form-error { - color: #990000; - display: inline-block; - font-size: 90%; - font-weight: bold; - padding: 10px 0; - } - - div.preview-toggle{ - padding: 15px 0; - width: auto; - a { - @extend .light-button; - } - } - - .wmd-preview { - margin: 3px 0 15px 0; - padding: 10px; - background-color: #F5F5F5; - min-height: 20px; - overflow: auto; - font-size: 13px; - font-family: Arial; - - p { - margin-bottom: 14px; - line-height: 1.4; - font-size: 14px; - } - - blockquote { - margin-left: 2.5%; - padding-left: 1.5%; - border-left: 1px dashed #ddd; - color: $pink; - } - - ul, ol, pre { - margin-left: 3%; - margin-bottom: 20px; - } - - - pre { - background-color: #eee; - } - - blockquote { - background-color: #eee; - } - } -} - -input.after-editor { - margin-bottom: 20px; - margin-right: 10px; -} - -form.question-form { - @extend .answer-form; - border: none; - padding: 15px 0 0 0; - - input[type="text"] { - @include box-sizing(border-box); - width: flex-grid(6); - } - - input[type="checkbox"] { - margin-top: 10px; - } - - input[value="Cancel"] { - @extend .light-button; - float: right; - } - - div#question-list { - background-color: rgba(255,255,255,0.95); - @include box-sizing(border-box); - margin-top: -15px; - max-width: 505px; - min-width: 300px; - overflow: hidden; - padding-left: 5px; - position: absolute; - width: 35%; - z-index: 9999; - - h2 { - text-transform: none; - padding: 8px 0; - border-bottom: 1px solid #eee; - margin: 0; - - span { - background: #eee; - color: #555; - padding: 2px 5px; - @include border-radius(2px); - margin-right: 5px; - } - } - } -} - - diff --git a/lms/static/sass/course/discussion/_modals.scss b/lms/static/sass/course/discussion/_modals.scss deleted file mode 100644 index 6c91956fc9..0000000000 --- a/lms/static/sass/course/discussion/_modals.scss +++ /dev/null @@ -1,33 +0,0 @@ -// Style for modal boxes that pop up to notify the user of various events -.vote-notification { - background-color: darken(#666, 7%); - @include border-radius(4px); - @include box-shadow(0px 2px 9px #aaa); - color: white; - cursor: pointer; - display: none; - font-size: 14px; - font-weight: normal; - padding-bottom: 10px; - position: absolute; - text-align: center; - z-index: 1; - - h3 { - background: #666; - padding: 10px 10px 10px 10px; - font-size: 13px; - margin-bottom: 5px; - border-bottom: darken(#666, 10%) 1px solid; - @include box-shadow(0 1px 0 lighten(#666, 10%)); - color: #fff; - font-weight: normal; - @include border-radius(4px 4px 0 0); - } - - a { - color: #fb7321; - text-decoration: underline; - font-weight: bold; - } -} diff --git a/lms/static/sass/course/discussion/_profile.scss b/lms/static/sass/course/discussion/_profile.scss deleted file mode 100644 index f20b51b72b..0000000000 --- a/lms/static/sass/course/discussion/_profile.scss +++ /dev/null @@ -1,119 +0,0 @@ -// Style for the user profile view - -body.user-profile-page { - - section.questions { - h1 { - margin: 0; - } - } - - ul.sub-info { - margin-top: lh(); - list-style: none; - padding: 0; - - > li { - display: table-cell; - padding: (flex-gutter(9)/2); - border-right: 1px dashed #efefef; - @include box-sizing(border-box); - - &:first-child { - padding-left: 0; - } - - &:last-child { - border-right: 0; - padding-right: 0; - } - - &.votes-badges { - width: flex-grid(2,9); - - p { - margin-top: 15px; - } - - } - - &.answer-list { - width: flex-grid(4, 9); - } - - &.tags-list { - width: flex-grid(3,9); - } - - h2 { - margin-bottom: 30px; - margin-top: 0; - } - - span.tag-number { - display: none; - } - } - - ul { - list-style: none; - padding: 0; - - &.user-stats-table { - list-style: none; - - li { - padding: 10px 0 15px; - border-top: 1px solid #eee; - } - } - - &.vote-buttons { - list-style: none; - margin-bottom: 30px; - - li { - background-position: 10px -10px; - background-repeat: no-repeat; - display: inline-block; - padding: 2px 10px 2px 40px; - margin-bottom: lh(.5); - border: 1px solid lighten($border-color, 10%); - - &.up { - background-image: url(../images/askbot/vote-arrow-up.png); - margin-right: 6px; - } - - &.down { - background-image: url(../images/askbot/vote-arrow-down.png); - } - } - } - - &.badges { - @include inline-block(); - padding: 0; - margin: 0; - - a { - background-color: #e3e3e3; - border: 0; - @include border-radius(4px); - color: #292309; - display: block; - font-size: 12px; - padding: 10px; - margin-bottom: 10px; - text-shadow: 0 1px 0 #fff; - text-transform: uppercase; - text-decoration: none; - - &:hover { - background-color: #cdcdcd; - } - } - } - } - } -} diff --git a/lms/static/sass/course/discussion/_question-view.scss b/lms/static/sass/course/discussion/_question-view.scss deleted file mode 100644 index 878fb8bede..0000000000 --- a/lms/static/sass/course/discussion/_question-view.scss +++ /dev/null @@ -1,377 +0,0 @@ -// Styles for the single question view - -div.question-header { - @include clearfix(); - - div.official-stamp { - background: $pink; - color: #fff; - font-size: 12px; - margin-left: -1px; - margin-top: 10px; - padding: 2px 5px; - text-align: center; - } - - div.vote-buttons { - display: inline-block; - float: left; - margin-right: flex-gutter(9); - width: flex-grid(0.7,9); - - ul { - padding: 0; - margin: 0; - - li { - background-repeat: no-repeat; - color: #999; - font-size: em(20); - font-weight: bold; - list-style: none; - text-align: center; - - &.question-img-upvote, &.answer-img-upvote { - background-image: url(../images/askbot/vote-arrow-up.png); - background-position: center 0; - cursor: pointer; - height: 12px; - margin-bottom: lh(.5); - - &:hover, &.on { - background-image: url(../images/askbot/vote-arrow-up.png); - background-position: center -22px; - } - } - - &.question-img-downvote, &.answer-img-downvote { - cursor: pointer; - background-image: url(../images/askbot/vote-arrow-down.png); - background-position: center 0; - height: 12px; - margin-top: lh(.5); - - &:hover, &.on { - background-image: url(../images/askbot/vote-arrow-down.png); - background-position: center -22px; - } - } - } - } - } - - div.question-container { - display: inline-block; - float: left; - width: flex-grid(8.3,9); - - h1 { - margin-top: 0; - font-weight: 100; - line-height: 1.1em; - - a { - font-weight: 100; - line-height: 1.1em; - } - } - - div.meta-bar { - border-bottom: 1px solid #eee; - display: block; - margin: lh(.5) 0 lh(); - overflow: hidden; - padding: 5px 0 10px; - - div.tag-list { - display: inline-block; - float:left; - width: flex-grid(4,8); - margin-right: flex-gutter(8); - } - - div.question-actions { - display: inline-block; - float:left; - text-align: right; - width: flex-grid(4,8); - - a { - @extend a:link; - cursor: pointer; - } - - span.sep { - color: #ccc; - } - } - } - - div.question-content { - overflow: hidden; - - div.question-body { - display: inline-block; - float: left; - margin-right: flex-gutter(8); - width: flex-grid(6.2,8); - - blockquote { - margin-left: 2.5%; - padding-left: 1.5%; - border-left: 1px dashed #ddd; - color: $pink; - } - - ul, ol, pre { - margin-left: 6%; - margin-bottom: 20px; - } - } - - - div.post-update-container { - display: inline-block; - float: left; - width: 20%; - border-left: 1px dashed #ddd; - - a { - border-bottom: none; - font-style: normal; - } - - div.post-update-info { - @include box-sizing(border-box); - padding: 10px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - &.revision { - text-align: center; - // background:lighten($cream, 7%); - - a { - color: black; - } - } - - div.change-date { - font-size: em(14); - margin-bottom: 2px; - } - - div.user-meta { - display: inline-block; - - span.username { - font-size: 20px; - margin-right: 5px; - } - - span.user-badges { - } - } - } - } - } - - div.comments-container { - @include box-sizing(border-box); - display: inline-block; - padding: 0 0 3% 0; - width: 100%; - margin-top: lh(2); - - div.comments-content { - border-top: 1px solid lighten($border-color, 10%); - - .block { - border-top: 1px solid lighten($border-color, 10%); - padding: 15px; - display: block; - - &:first-child { - border-top: 0; - } - - &.official { - padding-top: 10px; - - span.official-comment { - background: $pink; - color: #fff; - display: block; - font-size: em(12); - margin: 0 0 10px -5%; - padding:2px 5px 2px 5%; - text-align: left; - width:100px; - } - } - } - - form.post-comments { - padding: 15px; - - button:first-of-type { - @extend .blue-button; - } - - button:last-child { - margin-left: 10px; - float: right; - } - } - - div.comment { - &:first-child { - border-top: 0; - } - - &:last-child { - margin-bottom: 20px; - } - - aside.comment-controls { - background: none; - border: none; - @include box-shadow(none); - display: inline-block; - padding:0 2% 0 0; - text-align: center; - width: 5%; - - div { - background: none; - opacity: 0.6; - - &:hover { - opacity: 1; - } - } - - div.comment-votes { - width: 16px; - - a.upvote { - background: url(../images/askbot/comment-vote-up.png) no-repeat 2px; - cursor: pointer; - color: green; - display: block; - margin-bottom: 6px; - margin-top: 5px; - overflow: hidden; - text-decoration: none; - text-indent: -9999px; - width: 20px; - } - - a.upvoted { - @include border-radius(3px); - background: #D1E3A8; - color: green; - font-weight: bold; - margin-top: 10px; - padding: 2px; - text-indent: 0px; - } - } - - hr { - margin: 0; - } - - div.comment-delete { - @extend a:link; - cursor: pointer; - } - - div.comment-edit { - @include transform(rotate(50deg)); - cursor: pointer; - - a.edit-icon { - color: #555; - text-decoration: none; - } - } - } - - div.comment-body { - display: inline-block; - width: 95%; - - &#full-width { - width: 100%; - } - - div.comment-meta { - text-align: right; - margin-top: lh(.5); - - a.author { - font-weight: bold; - } - - a.edit { - padding: 2px 10px; - } - } - } - } - } - - #edit-comment-form { - margin: 10px 0; - min-height: 100px; - width: 99%; - resize: vertical; - } - .counter { - color: #888; - display: none; - float: right; - margin-top: 5px; - text-align: right; - } - - div.controls { - text-align: right; - - a { - display: inline-block; - margin: 10px 10px 10px 0; - } - } - } - } -} - -div.question-status { - background: $pink; - clear:both; - color: #fff; - display: block; - padding: 10px 0 10px 7.5%; - - h3 { - font-weight: normal; - } - - a { - color: #eee; - } -} - -div.share-question { - padding: 10px 0 10px 7.5%; - - p { - padding: 0; - margin: 0; - } -} diff --git a/lms/static/sass/course/discussion/_questions.scss b/lms/static/sass/course/discussion/_questions.scss deleted file mode 100644 index 1b77231bba..0000000000 --- a/lms/static/sass/course/discussion/_questions.scss +++ /dev/null @@ -1,264 +0,0 @@ -// Styles for the default question list view - -div.question-list-header { - @extend h1.top-header; - display: block; - margin-bottom: 0px; - padding-bottom: lh(.5); - overflow: hidden; - width: flex-grid(9,9); - - h1 { - margin: 0; - font-size: 1em; - font-weight: 100; - padding-bottom: lh(.5); - - > a.light-button { - float: right; - font-size: em(14, 24); - letter-spacing: 0; - font-weight: 400; - } - } - - section.question-list-meta { - display: block; - overflow: hidden; - width: 100%; - - div { - display: inline-block; - float: left; - } - - h1 { - margin: 0; - } - span.label { - color: #555; - } - - div.question-list-title { - margin-right: flex-gutter(); - - h1 { - margin-top: 0; - } - } - - - div.question-sort { - float: right; - margin-left: flex-gutter(); - margin-top: 6px; - - nav { - @extend .action-link; - float: right; - font-size: em(16, 24); - - a { - font-size: 1em; - - &.on span{ - font-weight: bold; - } - - &:before { - content: '|'; - color: #ccc; - font-size: 16px; - } - } - } - } - } - - section.question-tags-list { - display: block; - min-height: 26px; - padding-top:15px; - width: 100%; - - div { - display: inline-block; - float: left; - } - - div.back { - margin-right: 10px; - margin-top: 4px; - - a { - color: #555; - font-size: em(14, 24); - } - } - - div.tags-list { - - } - - ul.tags { - span, div { - line-height: 1em; - margin-left: 6px; - cursor: pointer; - } - } - } -} - -ul.question-list, div#question-list { - width: flex-grid(9,9); - padding-left: 0; - margin: 0; - - li.single-question { - border-bottom: 1px solid #eee; - list-style: none; - padding: lh() 0; - width: 100%; - - &:first-child { - border-top: 0; - } - - div { - display: inline-block; - - &.question-body { - @include box-sizing(border-box); - margin-right: flex-gutter(); - width: flex-grid(5,9); - - h2 { - font-size: em(20); - font-weight: bold; - letter-spacing: 0; - margin: 0 0 lh() 0; - text-transform: none; - line-height: lh(); - - a { - line-height: lh(); - } - } - - p.excerpt { - color: #777; - } - - - div.user-info { - display: inline-block; - vertical-align: top; - margin: lh() 0 0 0; - line-height: lh(); - - span.relative-time { - font-weight: normal; - line-height: lh(); - } - } - - ul.tags { - display: inline-block; - margin: lh() 0 0 0; - padding: 0; - } - - } - - &.question-meta { - float: right; - width: flex-grid(3,9); - - ul { - @include clearfix; - margin: 0; - padding: 0; - list-style: none; - - li { - border: 1px solid lighten($border-color, 10%); - @include box-sizing(border-box); - @include box-shadow(0 1px 0 #fff); - height:60px; - float: left; - margin-right: flex-gutter(3); - width: flex-grid(1,3); - - &:last-child { - margin-right: 0px; - } - - &:hover { - span, div { - color: #555; - } - } - - &.answers { - &.accepted { - border-color: lighten($border-color, 10%); - - span, div { - color: darken(#c4dfbe, 35%); - } - } - - &.no-answers { - span, div { - color: $pink; - } - } - } - - span, div { - @include box-sizing(border-box); - color: #888; - display: block; - text-align: center; - } - - span { - font-size: 16px; - font-weight: bold; - height: 35px; - padding-top: 15px; - vertical-align: middle; - } - - div { - height: 25px; - font-size: 12px; - } - } - } - } - } - - } - - div.post-own-question { - padding: 11px; - margin-top: 10px; - color: #888; - text-align: center; - - a { - font-weight: bold; - @extend .light-button; - padding: 20px; - display: block; - margin: 10px auto; - text-align: center; - width: flex-grid(5); - } - } -} - -.search-result-summary { -} diff --git a/lms/static/sass/course/discussion/_sidebar.scss b/lms/static/sass/course/discussion/_sidebar.scss deleted file mode 100644 index adcd38a383..0000000000 --- a/lms/static/sass/course/discussion/_sidebar.scss +++ /dev/null @@ -1,319 +0,0 @@ -// Styles for the Askbot sidebar - -div.discussion-wrapper aside { - @extend .sidebar; - border-left: 1px solid #ccc; - border-right: 0; - width: flex-grid(3); - border-radius: 0 3px 3px 0; - - &:after { - left: -1px; - right: auto; - } - - &.main-sidebar { - min-width:200px; - } - - h1 { - margin-bottom: 0; - } - - h2 { - color: #3C3C3C; - font-size: 1em; - font-style: normal; - font-weight: bold; - margin-bottom: 1em; - - &.first { - margin-top: 0px; - } - } - - h3 { - border-bottom: 0; - box-shadow: none; - } - - div.inputs { - input[type="submit"] { - width: 27%; - float: right; - text-align: center; - padding: 4px 0; - text-transform: capitalize; - } - - input[type="text"] { - width: 62%; - } - } - - div.box { - display: block; - padding: 18px 26px; - border-top: 1px solid lighten($border-color, 10%); - - &:first-child { - border-top: 0; - } - - ul#related-tags { - position: relative; - left: -10px; - - li { - border-bottom: 0; - background: #ddd; - padding: 6px 10px 6px 5px; - - a { - padding: 0; - line-height: 12px; - - &:hover { - background: transparent; - } - } - } - } - - &.contributors { - - a { - @include border-radius(3px); - border: 1px solid #aaa; - cursor: pointer; - display: inline-block; - margin-right: 6px; - position: relative; - - &:before { - @include border-radius(3px); - @include box-shadow(inset 0 0 1px 1px rgba(255,255,255,.4)); - top: 1px; left: 1px; bottom: 1px; right: 1px; - content: ''; - position: absolute; - } - - } - } - - &.tag-selector { - ul { - margin-bottom: 10px; - display: block; - } - } - } - - div.search-box { - margin-top: lh(.5); - - input { - @include box-sizing(border-box); - display: inline; - } - - input[type='submit'] { - background: url(../images/askbot/search-icon.png) no-repeat center; - border: 0; - @include box-shadow(none); - margin-left: 3px; - opacity: 0.5; - padding: 6px 0 0; - position: absolute; - text-indent: -9999px; - width: 24px; - - &:hover { - opacity: 0.9; - } - - &:focus { - opacity: 1; - } - } - - input#keywords { - padding-left: 30px; - padding-right: 30px; - width: 100%; - } - - input#clear { - background: none; - border: none; - @include border-radius(0); - @include box-shadow(none); - color: #999; - display: inline; - font-size: 12px; - font-weight: bold; - height: 19px; - line-height: 1em; - margin: { - left: -25px; - top: 8px; - } - padding: 2px 5px; - text-shadow: none; - } - } - - div#tagSelector { - ul { - margin: 0; - } - - div.inputs { - margin-bottom: lh(); - } - - div#displayTagFilterControl { - p.choice { - @include inline-block(); - margin-right: lh(.5); - margin-top: 0; - } - } - - label { - font-style: normal; - font-weight: 400; - } - } - - // Question view specific - - div.follow-buttons { - margin-top: 20px; - display: block; - - a.button { - @include box-sizing(border-box); - display: block; - text-align: center; - width: 100%; - } - } - - - div.question-stats { - border-top: 0; - - ul { - color: #777; - list-style: none; - - li { - padding: 7px 0 0; - border: 0; - - &:last-child { - @include box-shadow(none); - border: 0; - } - strong { - float: right; - padding-right: 10px; - } - } - } - } - - div.user-info, div.user-stats { - @extend div.question-stats; - overflow: hidden; - - div { - float: left; - display: block; - } - - div.karma { - border: 1px solid $border-color; - @include box-sizing(border-box); - padding: lh(.4) 0; - text-align: center; - width: flex-grid(1, 3); - float: right; - - p { - text-align: center; - - strong { - display: block; - font-style: 20px; - } - } - } - - div.meta { - width: flex-grid(2,3); - padding-right: flex-gutter(3)*0.5; - @include box-sizing(border-box); - - h2 { - border: 0; - @include box-shadow(none); - margin: 0 0 8px 0; - padding: 0; - } - - p { - color: #777; - font-size: 14px; - } - } - } - - div.user-stats { - overflow: visible; - - ul { - h2 { - margin:0 (-(lh())) 5px (-(lh())); - padding: lh(.5) lh(); - } - } - } - - div.question-tips, div.markdown { - ul, - ol { - margin: 0; - padding: 0; - - li { - border-bottom: 0; - line-height: lh(); - margin-bottom: em(8); - } - } - } - - div.view-profile { - border-top: 0; - padding-top: 0; - - a { - @extend .gray-button; - @include box-sizing(border-box); - display: block; - text-align: center; - width: 100%; - margin-top: lh(.5); - - &:first-child { - margin-top: 0; - } - - span { - font-weight: bold; - } - } - } -} diff --git a/lms/static/sass/course/discussion/_tags.scss b/lms/static/sass/course/discussion/_tags.scss deleted file mode 100644 index 92210d8053..0000000000 --- a/lms/static/sass/course/discussion/_tags.scss +++ /dev/null @@ -1,44 +0,0 @@ -// Styles for the question tags - -ul.tags { - list-style: none; - display: inline; - padding: 0; - - li, a { - position: relative; - } - - li { - background: #ddd; - color: #555; - display: inline-block; - font-size: 12px; - margin-bottom: 5px; - margin-left: 15px; - padding: 6px 10px 6px 5px; - - &:before { - border-color:transparent #ddd transparent transparent; - border-style:solid; - border-width:12px 10px 12px 0; - content:""; - height:0; - left:-10px; - position:absolute; - top:0; - width:0; - } - - a { - color: #555; - text-decoration: none; - border-bottom: none; - font-style: normal; - } - } -} - -span.tag-number { - display: none; -} From 2865c7cad49dffb880515e2ec7c1f4dd4747d246 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 28 Nov 2012 16:57:30 +0200 Subject: [PATCH 038/736] moved tests from miller to tests --- common/lib/capa/capa/chem/miller.py | 149 ---------------------------- common/lib/capa/capa/chem/tests.py | 147 ++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 154 deletions(-) diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py index 77c10dd350..4c10e60ecc 100644 --- a/common/lib/capa/capa/chem/miller.py +++ b/common/lib/capa/capa/chem/miller.py @@ -4,7 +4,6 @@ import numpy as np import math import fractions as fr import decimal -import unittest import json @@ -266,151 +265,3 @@ def grade(user_input, correct_answer): return True return False - - -class Test_Crystallography_Miller(unittest.TestCase): - ''' Tests for crystallography grade function.''' - - def test_empty_points(self): - user_input = '{"lattice": "bcc", "points": []}' - self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_only_one_point(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}' - self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_only_two_points(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}' - self.assertFalse(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_1(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' - self.assertTrue(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_2(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'})) - - def test_3(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_4(self): - user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}' - self.assertTrue(grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'})) - - def test_5(self): - """ return true only in case points coordinates are exact. - But if they transform to closest 0.05 value it is not true""" - user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}' - self.assertFalse(grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) - - def test_6(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}' - self.assertTrue(grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'})) - - def test_7(self): # goes throug origin - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'})) - - def test_8(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'})) - - def test_9(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'})) - - def test_10(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) - - def test_11(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'})) - - def test_12(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'})) - - def test_13(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'})) - - def test_14(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'})) - - def test_15(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) - - def test_16(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) - - def test_17(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'})) - - def test_18(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) - - def test_19(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'})) - - def test_20(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'})) - - def test_21(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'})) - - def test_22(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'})) - - def test_23(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) - - def test_24(self): - user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}' - self.assertTrue(grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'})) - - def test_25(self): - user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''})) - - def test_26(self): - user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}' - self.assertTrue(grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''})) - - def test_27(self): - """ rounding to 0.35""" - user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}' - self.assertTrue(grade(user_input, {'miller': '(3,3,3)', 'lattice': ''})) - - def test_28(self): - """ rounding to 0.30""" - user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}' - self.assertTrue(grade(user_input, {'miller': '(10,10,10)', 'lattice': ''})) - - def test_wrong_lattice(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertFalse(grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) - - -def suite(): - - testcases = [Test_Crystallography_Miller] - suites = [] - for testcase in testcases: - suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) - return unittest.TestSuite(suites) - -if __name__ == "__main__": - unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py index 34d903ec1d..571526f915 100644 --- a/common/lib/capa/capa/chem/tests.py +++ b/common/lib/capa/capa/chem/tests.py @@ -1,13 +1,15 @@ import codecs from fractions import Fraction -from pyparsing import ParseException import unittest from chemcalc import (compare_chemical_expression, divide_chemical_expression, render_to_html, chemical_equations_equal) +import miller + local_debug = None + def log(s, output_type=None): if local_debug: print s @@ -37,7 +39,6 @@ class Test_Compare_Equations(unittest.TestCase): self.assertFalse(chemical_equations_equal('2H2 + O2 -> H2O2', '2O2 + 2H2 -> 2H2O2')) - def test_different_arrows(self): self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', '2O2 + 2H2 -> 2H2O2')) @@ -56,7 +57,6 @@ class Test_Compare_Equations(unittest.TestCase): self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 -> H2O2', exact=True)) - def test_syntax_errors(self): self.assertFalse(chemical_equations_equal('H2 + O2 a-> H2O2', '2O2 + 2H2 -> 2H2O2')) @@ -311,7 +311,6 @@ class Test_Render_Equations(unittest.TestCase): log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) - def test_render_eq3(self): s = "H^+ + OH^- <= H2O" # unsupported arrow out = render_to_html(s) @@ -320,10 +319,148 @@ class Test_Render_Equations(unittest.TestCase): self.assertEqual(out, correct) +class Test_Crystallography_Miller(unittest.TestCase): + ''' Tests for crystallography grade function.''' + + def test_empty_points(self): + user_input = '{"lattice": "bcc", "points": []}' + self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_only_one_point(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}' + self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_only_two_points(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}' + self.assertFalse(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_1(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_2(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'})) + + def test_3(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) + + def test_4(self): + user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'})) + + def test_5(self): + """ return true only in case points coordinates are exact. + But if they transform to closest 0.05 value it is not true""" + user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}' + self.assertFalse(miller.grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) + + def test_6(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'})) + + def test_7(self): # goes throug origin + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'})) + + def test_8(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'})) + + def test_9(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'})) + + def test_10(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) + + def test_11(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'})) + + def test_12(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'})) + + def test_13(self): + user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'})) + + def test_14(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'})) + + def test_15(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) + + def test_16(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) + + def test_17(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'})) + + def test_18(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) + + def test_19(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'})) + + def test_20(self): + user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'})) + + def test_21(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'})) + + def test_22(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'})) + + def test_23(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) + + def test_24(self): + user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'})) + + def test_25(self): + user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''})) + + def test_26(self): + user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''})) + + def test_27(self): + """ rounding to 0.35""" + user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': ''})) + + def test_28(self): + """ rounding to 0.30""" + user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}' + self.assertTrue(miller.grade(user_input, {'miller': '(10,10,10)', 'lattice': ''})) + + def test_wrong_lattice(self): + user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' + self.assertFalse(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) + def suite(): - testcases = [Test_Compare_Expressions, Test_Divide_Expressions, Test_Render_Equations] + testcases = [Test_Compare_Expressions, + Test_Divide_Expressions, + Test_Render_Equations, + Test_Crystallography_Miller] suites = [] for testcase in testcases: suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) From 74b30d8c204dfe4d40e80365a97b9d33d93489a9 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 28 Nov 2012 17:02:07 +0200 Subject: [PATCH 039/736] removed no-longer relevant template params --- common/lib/capa/capa/tests/test_inputtypes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 826d304717..dafd31bdc7 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -407,13 +407,11 @@ class CrystallographyTest(unittest.TestCase): def test_rendering(self): height = '12' width = '33' - size = '10' xml_str = """""".format(h=height, w=width, s=size) + />""".format(h=height, w=width) element = etree.fromstring(xml_str) @@ -428,9 +426,7 @@ class CrystallographyTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', - 'size': size, 'msg': '', - 'hidden': '', 'width': width, 'height': height, } From a0a516720782cfbb4858996f6150049a89ba95f8 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 11:12:06 -0500 Subject: [PATCH 040/736] Hook up testing for self assessment modules --- .../xmodule/xmodule/self_assessment_module.py | 25 +++++-------- common/lib/xmodule/xmodule/tests/__init__.py | 2 +- .../xmodule/xmodule/tests/test_progress.py | 4 +-- .../xmodule/tests/test_self_assessment.py | 35 +++++++++++++++++++ 4 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 common/lib/xmodule/xmodule/tests/test_self_assessment.py diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index 2edf5467b2..d76786007a 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -109,15 +109,13 @@ class SelfAssessmentModule(XModule): self.state = instance_state.get('state', 'initial') + self.attempts = instance_state.get('attempts', 0) + self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) + # Used for progress / grading. Currently get credit just for # completion (doesn't matter if you self-assessed correct/incorrect). - self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) - self.attempts = instance_state.get('attempts', 0) - - self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) - self.rubric = definition['rubric'] self.prompt = definition['prompt'] self.submit_message = definition['submitmessage'] @@ -149,26 +147,19 @@ class SelfAssessmentModule(XModule): # cdodge: perform link substitutions for any references to course static content (e.g. images) return rewrite_links(html, self.rewrite_content_links) - def get_score(self): - """ - Returns dict with 'score' key - """ - return {'score': self.get_last_score()} - def max_score(self): """ Return max_score """ return self._max_score - def get_last_score(self): + def get_score(self): """ Returns the last score in the list """ - last_score=0 - if(len(self.scores)>0): - last_score=self.scores[len(self.scores)-1] - return last_score + if len(self.scores) > 0: + return self.scores[-1] + return 0 def get_progress(self): ''' @@ -176,7 +167,7 @@ class SelfAssessmentModule(XModule): ''' if self._max_score > 0: try: - return Progress(self.get_last_score(), self._max_score) + return Progress(self.get_score(), self._max_score) except Exception as err: log.exception("Got bad progress") return None diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index ed64c45118..80b933e06d 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -19,7 +19,7 @@ import xmodule from xmodule.x_module import ModuleSystem from mock import Mock -i4xs = ModuleSystem( +test_system = ModuleSystem( ajax_url='courses/course_id/modx/a_location', track_function=Mock(), get_module=Mock(), diff --git a/common/lib/xmodule/xmodule/tests/test_progress.py b/common/lib/xmodule/xmodule/tests/test_progress.py index 94a0a19d7c..cb011cdc2b 100644 --- a/common/lib/xmodule/xmodule/tests/test_progress.py +++ b/common/lib/xmodule/xmodule/tests/test_progress.py @@ -5,7 +5,7 @@ import unittest from xmodule.progress import Progress from xmodule import x_module -from . import i4xs +from . import test_system class ProgressTest(unittest.TestCase): ''' Test that basic Progress objects work. A Progress represents a @@ -133,6 +133,6 @@ class ModuleProgressTest(unittest.TestCase): ''' def test_xmodule_default(self): '''Make sure default get_progress exists, returns None''' - xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {}) + xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {}) p = xm.get_progress() self.assertEqual(p, None) diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py new file mode 100644 index 0000000000..701a7f15fe --- /dev/null +++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py @@ -0,0 +1,35 @@ +import json +from mock import Mock +import unittest + +from xmodule.self_assessment_module import SelfAssessmentModule +from xmodule.modulestore import Location + +from . import test_system + +class SelfAssessmentTest(unittest.TestCase): + + definition = {'rubric': 'A rubric', + 'prompt': 'Who?', + 'submitmessage': 'Shall we submit now?', + 'hintprompt': 'Consider this...'} + + location = Location(["i4x", "edX", "sa_test", "selfassessment", + "SampleQuestion"]) + + metadata = {} + + descriptor = Mock() + + def test_import(self): + state = json.dumps({'student_answers': [], + 'scores': [], + 'hints': [], + 'state': 'initial', + 'attempts': 0}) + + module = SelfAssessmentModule(test_system, self.location, + self.definition, self.descriptor, + state, {}) + + self.assertEqual(module.get_score(), 0) From f8e45e0a8ea4549cf8873d6d23e01d746048f96e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 13:21:48 -0500 Subject: [PATCH 041/736] Add tests for old logic --- common/lib/xmodule/xmodule/tests/__init__.py | 5 ++- .../xmodule/tests/test_self_assessment.py | 38 ++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 80b933e06d..a07f1ddfaf 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -4,7 +4,7 @@ unittests for xmodule Run like this: rake test_common/lib/xmodule - + """ import unittest @@ -23,7 +23,8 @@ test_system = ModuleSystem( ajax_url='courses/course_id/modx/a_location', track_function=Mock(), get_module=Mock(), - render_template=Mock(), + # "render" to just the context... + render_template=lambda template, context: str(context), replace_urls=Mock(), user=Mock(), filestore=Mock(), diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py index 701a7f15fe..0ddcda8764 100644 --- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py +++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py @@ -12,24 +12,44 @@ class SelfAssessmentTest(unittest.TestCase): definition = {'rubric': 'A rubric', 'prompt': 'Who?', 'submitmessage': 'Shall we submit now?', - 'hintprompt': 'Consider this...'} + 'hintprompt': 'Consider this...', + } location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) - metadata = {} + metadata = {'attempts': '10'} descriptor = Mock() def test_import(self): - state = json.dumps({'student_answers': [], - 'scores': [], - 'hints': [], - 'state': 'initial', - 'attempts': 0}) + state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"], + 'scores': [0, 1], + 'hints': ['o hai'], + 'state': SelfAssessmentModule.ASSESSING, + 'attempts': 2}) module = SelfAssessmentModule(test_system, self.location, self.definition, self.descriptor, - state, {}) + state, {}, metadata=self.metadata) - self.assertEqual(module.get_score(), 0) + self.assertEqual(module.get_score(), 1) + + + self.assertTrue('answer 3' in module.get_html()) + self.assertFalse('answer 2' in module.get_html()) + + module.save_assessment({'assessment': '0'}) + self.assertEqual(module.state, module.REQUEST_HINT) + + module.save_hint({'hint': 'hint for ans 3'}) + self.assertEqual(module.state, module.DONE) + + d = module.reset({}) + self.assertTrue(d['success']) + self.assertEqual(module.state, module.INITIAL) + + # if we now assess as right, skip the REQUEST_HINT state + module.save_answer({'student_answer': 'answer 4'}) + module.save_assessment({'assessment': '1'}) + self.assertEqual(module.state, module.DONE) From e3b9238eefc6debfdec5d696a2b3b81fd2ccceed Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 14:08:18 -0500 Subject: [PATCH 042/736] reorder imports --- common/lib/xmodule/xmodule/self_assessment_module.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index d76786007a..d8994d1187 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -7,20 +7,21 @@ Parses xml definition file--see below for exact format. import copy from fs.errors import ResourceNotFoundError +import itertools +import json import logging -import os -import sys from lxml import etree from lxml.html import rewrite_links from path import path -import json -from progress import Progress +import os +import sys from pkg_resources import resource_string from .capa_module import only_one, ComplexEncoder from .editing_module import EditingDescriptor from .html_checker import check_html +from progress import Progress from .stringify import stringify_children from .x_module import XModule from .xml_module import XmlDescriptor From 4fd1a2fa1a6d2f04ba15aa53663e30c2fc878161 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 14:09:09 -0500 Subject: [PATCH 043/736] Fix state tracking for self-assessment. now keep each set of responses in a separate dictionary --- .../xmodule/xmodule/self_assessment_module.py | 139 ++++++++++++++---- .../xmodule/tests/test_self_assessment.py | 3 +- 2 files changed, 112 insertions(+), 30 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index d8994d1187..cf9e4a88c1 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -53,6 +53,8 @@ class SelfAssessmentModule(XModule): submissions too.) """ + STATE_VERSION = 1 + # states INITIAL = 'initial' ASSESSING = 'assessing' @@ -103,10 +105,13 @@ class SelfAssessmentModule(XModule): else: instance_state = {} - # Note: score responses are on scale from 0 to max_score - self.student_answers = instance_state.get('student_answers', []) - self.scores = instance_state.get('scores', []) - self.hints = instance_state.get('hints', []) + instance_state = self.convert_state_to_current_format(instance_state) + + # History is a list of tuples of (answer, score, hint), where hint may be + # None for any element, and score and hint can be None for the last (current) + # element. + # Scores are on scale from 0 to max_score + self.history = instance_state.get('history', []) self.state = instance_state.get('state', 'initial') @@ -122,14 +127,95 @@ class SelfAssessmentModule(XModule): self.submit_message = definition['submitmessage'] self.hint_prompt = definition['hintprompt'] + + def latest_answer(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('answer') + + def latest_score(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('score') + + def latest_hint(self): + """None if not available""" + if not self.history: + return None + return self.history[-1].get('hint') + + def new_history_entry(self, answer): + self.history.append({'answer': answer}) + + def record_latest_score(self, score): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['score'] = score + + def record_latest_hint(self, hint): + """Assumes that state is right, so we're adding a score to the latest + history element""" + self.history[-1]['hint'] = hint + + + @staticmethod + def convert_state_to_current_format(old_state): + """ + This module used to use a problematic state representation. This method + converts that into the new format. + + Args: + old_state: dict of state, as passed in. May be old. + + Returns: + new_state: dict of new state + """ + if old_state.get('version', 0) == SelfAssessmentModule.STATE_VERSION: + # already current + return old_state + + # for now, there's only one older format. + + new_state = {'version': SelfAssessmentModule.STATE_VERSION} + + def copy_if_present(key): + if key in old_state: + new_state[key] = old_state[key] + + for to_copy in ['attempts', 'state']: + copy_if_present(to_copy) + + # The answers, scores, and hints need to be kept together to avoid them + # getting out of sync. + + # NOTE: Since there's only one problem with a few hundred submissions + # in production so far, not trying to be smart about matching up hints + # and submissions in cases where they got out of sync. + + student_answers = old_state.get('student_answers', []) + scores = old_state.get('scores', []) + hints = old_state.get('hints', []) + + new_state['history'] = [ + {'answer': answer, + 'score': score, + 'hint': hint} + for answer, score, hint in itertools.izip_longest( + student_answers, scores, hints)] + return new_state + + def _allow_reset(self): """Can the module be reset?""" return self.state == self.DONE and self.attempts < self.max_attempts def get_html(self): #set context variables and render template - if self.state != self.INITIAL and self.student_answers: - previous_answer = self.student_answers[-1] + if self.state != self.INITIAL: + latest = self.latest_answer() + previous_answer = latest if latest is not None else '' else: previous_answer = '' @@ -158,9 +244,9 @@ class SelfAssessmentModule(XModule): """ Returns the last score in the list """ - if len(self.scores) > 0: - return self.scores[-1] - return 0 + score = self.latest_score() + return {'score': score if score is not None else 0, + 'total': self._max_score} def get_progress(self): ''' @@ -168,7 +254,7 @@ class SelfAssessmentModule(XModule): ''' if self._max_score > 0: try: - return Progress(self.get_score(), self._max_score) + return Progress(self.get_score()['score'], self._max_score) except Exception as err: log.exception("Got bad progress") return None @@ -242,9 +328,10 @@ class SelfAssessmentModule(XModule): if self.state in (self.INITIAL, self.ASSESSING): return '' - if self.state == self.DONE and len(self.hints) > 0: + if self.state == self.DONE: # display the previous hint - hint = self.hints[-1] + latest = self.latest_hint() + hint = latest if latest is not None else '' else: hint = '' @@ -287,7 +374,8 @@ class SelfAssessmentModule(XModule): if self.state != self.INITIAL: return self.out_of_sync_error(get) - self.student_answers.append(get['student_answer']) + # add new history element with answer and empty score and hint. + self.new_history_entry(get['student_answer']) self.state = self.ASSESSING return { @@ -310,18 +398,15 @@ class SelfAssessmentModule(XModule): 'message_html' only if success is true """ - n_answers = len(self.student_answers) - n_scores = len(self.scores) - if (self.state != self.ASSESSING or n_answers != n_scores + 1): - msg = "%d answers, %d scores" % (n_answers, n_scores) - return self.out_of_sync_error(get, msg) + if self.state != self.ASSESSING: + return self.out_of_sync_error(get) try: score = int(get['assessment']) - except: + except ValueError: return {'success': False, 'error': "Non-integer score value"} - self.scores.append(score) + self.record_latest_score(score) d = {'success': True,} @@ -352,7 +437,7 @@ class SelfAssessmentModule(XModule): # the same number of hints and answers. return self.out_of_sync_error(get) - self.hints.append(get['hint'].lower()) + self.record_latest_hint(get['hint']) self.state = self.DONE # increment attempts @@ -362,9 +447,8 @@ class SelfAssessmentModule(XModule): event_info = { 'selfassessment_id': self.location.url(), 'state': { - 'student_answers': self.student_answers, - 'score': self.scores, - 'hints': self.hints, + 'version': self.STATE_VERSION, + 'history': self.history, } } self.system.track_function('save_hint', event_info) @@ -399,12 +483,11 @@ class SelfAssessmentModule(XModule): """ state = { - 'student_answers': self.student_answers, - 'hints': self.hints, + 'version': self.STATE_VERSION, + 'history': self.history, 'state': self.state, - 'scores': self.scores, 'max_score': self._max_score, - 'attempts': self.attempts + 'attempts': self.attempts, } return json.dumps(state) diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py index 0ddcda8764..055f75ed97 100644 --- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py +++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py @@ -33,8 +33,7 @@ class SelfAssessmentTest(unittest.TestCase): self.definition, self.descriptor, state, {}, metadata=self.metadata) - self.assertEqual(module.get_score(), 1) - + self.assertEqual(module.get_score(), 0) self.assertTrue('answer 3' in module.get_html()) self.assertFalse('answer 2' in module.get_html()) From b41b45971256f314887019dbb46bef1ea49fa0e6 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 14:09:32 -0500 Subject: [PATCH 044/736] update docstring on x_module.get_score to match reality --- common/lib/xmodule/xmodule/x_module.py | 38 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 2b2e709bcb..6f3fb73356 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -233,17 +233,17 @@ class XModule(HTMLSnippet): self._loaded_children = [c for c in children if c is not None] return self._loaded_children - + def get_children_locations(self): ''' Returns the locations of each of child modules. - + Overriding this changes the behavior of get_children and anything that uses get_children, such as get_display_items. - + This method will not instantiate the modules of the children unless absolutely necessary, so it is cheaper to call than get_children - + These children will be the same children returned by the descriptor unless descriptor.has_dynamic_children() is true. ''' @@ -288,8 +288,20 @@ class XModule(HTMLSnippet): return '{}' def get_score(self): - ''' Score the student received on the problem. - ''' + """ + Score the student received on the problem, or None if there is no + score. + + Returns: + dictionary + {'score': integer, from 0 to get_max_score(), + 'total': get_max_score()} + + NOTE (vshnayder): not sure if this was the intended return value, but + that's what it's doing now. I suspect that we really want it to just + return a number. Would need to change (at least) capa and + modx_dispatch to match if we did that. + """ return None def max_score(self): @@ -319,7 +331,7 @@ class XModule(HTMLSnippet): get is a dictionary-like object ''' return "" - # cdodge: added to support dynamic substitutions of + # cdodge: added to support dynamic substitutions of # links for courseware assets (e.g. images). is passed through from lxml.html parser def rewrite_content_links(self, link): # see if we start with our format, e.g. 'xasset:' @@ -408,7 +420,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): # cdodge: this is a list of metadata names which are 'system' metadata # and should not be edited by an end-user system_metadata_fields = [ 'data_dir' ] - + # A list of descriptor attributes that must be equal for the descriptors to # be equal equality_attributes = ('definition', 'metadata', 'location', @@ -562,18 +574,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): self, metadata=self.metadata ) - - + + def has_dynamic_children(self): """ Returns True if this descriptor has dynamic children for a given student when the module is created. - + Returns False if the children of this descriptor are the same - children that the module will return for any student. + children that the module will return for any student. """ return False - + # ================================= JSON PARSING =========================== @staticmethod From 0a1b731dbcd660db2dabb4144948093c564d7d1c Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 14:18:44 -0500 Subject: [PATCH 045/736] properly reset answer area on module reset --- common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee index 951eb42fce..5b70ab29aa 100644 --- a/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/selfassessment/display.coffee @@ -120,7 +120,7 @@ class @SelfAssessment if @state == 'done' $.postWithPrefix "#{@ajax_url}/reset", {}, (response) => if response.success - @answer_area.html('') + @answer_area.val('') @rubric_wrapper.html('') @hint_wrapper.html('') @message_wrapper.html('') From 4b58cb956084f022511e4f03bbef24c486b9e229 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 28 Nov 2012 15:04:07 -0500 Subject: [PATCH 046/736] Fix attempt tracking, fix test - increments attempts on any transition to DONE state --- .../xmodule/xmodule/self_assessment_module.py | 26 +++++++++++++------ .../xmodule/tests/test_self_assessment.py | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/common/lib/xmodule/xmodule/self_assessment_module.py b/common/lib/xmodule/xmodule/self_assessment_module.py index cf9e4a88c1..8498a210cd 100644 --- a/common/lib/xmodule/xmodule/self_assessment_module.py +++ b/common/lib/xmodule/xmodule/self_assessment_module.py @@ -160,6 +160,19 @@ class SelfAssessmentModule(XModule): self.history[-1]['hint'] = hint + def change_state(self, new_state): + """ + A centralized place for state changes--allows for hooks. If the + current state matches the old state, don't run any hooks. + """ + if self.state == new_state: + return + + self.state = new_state + + if self.state == self.DONE: + self.attempts += 1 + @staticmethod def convert_state_to_current_format(old_state): """ @@ -376,7 +389,7 @@ class SelfAssessmentModule(XModule): # add new history element with answer and empty score and hint. self.new_history_entry(get['student_answer']) - self.state = self.ASSESSING + self.change_state(self.ASSESSING) return { 'success': True, @@ -411,11 +424,11 @@ class SelfAssessmentModule(XModule): d = {'success': True,} if score == self.max_score(): - self.state = self.DONE + self.change_state(self.DONE) d['message_html'] = self.get_message_html() d['allow_reset'] = self._allow_reset() else: - self.state = self.REQUEST_HINT + self.change_state(self.REQUEST_HINT) d['hint_html'] = self.get_hint_html() d['state'] = self.state @@ -438,10 +451,7 @@ class SelfAssessmentModule(XModule): return self.out_of_sync_error(get) self.record_latest_hint(get['hint']) - self.state = self.DONE - - # increment attempts - self.attempts = self.attempts + 1 + self.change_state(self.DONE) # To the tracking logs! event_info = { @@ -473,7 +483,7 @@ class SelfAssessmentModule(XModule): 'success': False, 'error': 'Too many attempts.' } - self.state = self.INITIAL + self.change_state(self.INITIAL) return {'success': True} diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py index 055f75ed97..d89190b1e0 100644 --- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py +++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py @@ -33,7 +33,7 @@ class SelfAssessmentTest(unittest.TestCase): self.definition, self.descriptor, state, {}, metadata=self.metadata) - self.assertEqual(module.get_score(), 0) + self.assertEqual(module.get_score()['score'], 0) self.assertTrue('answer 3' in module.get_html()) self.assertFalse('answer 2' in module.get_html()) From 6a24ecebe7e2eadf9b89d2054ad608cbd94144d1 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 28 Nov 2012 17:54:18 -0500 Subject: [PATCH 047/736] decode utf-8 when reading html, and encode when writing. --- common/djangoapps/mitxmako/shortcuts.py | 2 +- common/djangoapps/mitxmako/template.py | 2 +- common/lib/xmodule/xmodule/html_module.py | 4 ++-- common/lib/xmodule/xmodule/template_module.py | 2 +- common/lib/xmodule/xmodule/xml_module.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index ba22f2db20..181d3befd5 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -42,7 +42,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary.update(context) # fetch and render template template = middleware.lookup[namespace].get_template(template_name) - return template.render(**context_dictionary) + return template.render_unicode(**context_dictionary) def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py index 56096fe173..2d6fc026ca 100644 --- a/common/djangoapps/mitxmako/template.py +++ b/common/djangoapps/mitxmako/template.py @@ -54,5 +54,5 @@ class Template(MakoTemplate): context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL context_dictionary['django_context'] = context_instance - return super(Template, self).render(**context_dictionary) + return super(Template, self).render_unicode(**context_dictionary) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 2023ac7017..4f10cc84f1 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -123,7 +123,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): try: with system.resources_fs.open(filepath) as file: - html = file.read() + html = file.read().decode('utf-8') # Log a warning if we can't parse the file, but don't error if not check_html(html): msg = "Couldn't parse html in {0}.".format(filepath) @@ -164,7 +164,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(self.definition['data']) + file.write(self.definition['data'].encode('utf-8')) # write out the relative name relname = path(pathname).basename() diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py index 13eab038ec..d3fb0aab5e 100644 --- a/common/lib/xmodule/xmodule/template_module.py +++ b/common/lib/xmodule/xmodule/template_module.py @@ -58,7 +58,7 @@ class CustomTagDescriptor(RawDescriptor): params = dict(xmltree.items()) with system.resources_fs.open('custom_tags/{name}' .format(name=template_name)) as template: - return Template(template.read()).render(**params) + return Template(template.read().decode('utf-8')).render(**params) def __init__(self, system, definition, **kwargs): diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index ec755af4ef..e65a8c74ea 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -366,7 +366,7 @@ class XmlDescriptor(XModuleDescriptor): filepath = self.__class__._format_filepath(self.category, url_path) resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(etree.tostring(xml_object, pretty_print=True)) + file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8', xml_declaration=True)) # And return just a pointer with the category and filename. record_object = etree.Element(self.category) From 9bfa800208de908792e424cf21ff4adfb06b7707 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Wed, 28 Nov 2012 18:28:40 -0500 Subject: [PATCH 048/736] add non-ascii data to toy dataset --- common/test/data/toy/html/secret/toylab.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/test/data/toy/html/secret/toylab.html b/common/test/data/toy/html/secret/toylab.html index 81df84bd63..760482c4a0 100644 --- a/common/test/data/toy/html/secret/toylab.html +++ b/common/test/data/toy/html/secret/toylab.html @@ -1,3 +1,9 @@ Lab 2A: Superposition Experiment

Isn't the toy course great?

+ +

Let's add some markup that uses non-ascii characters. +For example, we should be able to write words like encyclopædia, or foreign words like français. +Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. +And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π. +

From 796369c13786b0fb5b13c4824cb4d0f85d5cfd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 27 Nov 2012 14:02:35 -0500 Subject: [PATCH 049/736] Fix installation of coffeescript and ruby on linux --- create-dev-env.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/create-dev-env.sh b/create-dev-env.sh index 5edc765e4f..fc8e830760 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -99,7 +99,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev" +APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev coffeescript libreadline6 libreadline6-dev" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" @@ -182,16 +182,19 @@ case `uname -s` in error "Please install lsb-release." exit 1 } + distro=`lsb_release -cs` case $distro in maya|lisa|natty|oneiric|precise|quantal) output "Installing ubuntu requirements" + sudo apt-get install python-software-properties sudo add-apt-repository ppa:chris-lea/node.js sudo apt-get -y update + # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation sudo DEBIAN_FRONTEND=noninteractive apt-get -y install $APT_PKGS - sudo npm install coffee-script + clone_repos ;; *) @@ -272,7 +275,7 @@ output "Installing rvm and ruby" curl -sL get.rvm.io | bash -s -- --version 1.15.7 source $RUBY_DIR/scripts/rvm # skip the intro -LESS="-E" rvm install $RUBY_VER +LESS="-E" rvm install $RUBY_VER --with-readline output "Installing gem bundler" gem install bundler output "Installing ruby packages" From 41142ab56aa2f1a14838de24d6e2dd0dedc23da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Tue, 27 Nov 2012 14:16:19 -0500 Subject: [PATCH 050/736] Add mongodb to create-dev-env.sh script --- brew-formulas.txt | 1 + create-dev-env.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/brew-formulas.txt b/brew-formulas.txt index e06829a43a..061297edc5 100644 --- a/brew-formulas.txt +++ b/brew-formulas.txt @@ -9,3 +9,4 @@ node graphviz mysql geos +mongodb diff --git a/create-dev-env.sh b/create-dev-env.sh index fc8e830760..c30f0f3732 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -99,7 +99,7 @@ NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev coffeescript libreadline6 libreadline6-dev" +APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev coffeescript libreadline6 libreadline6-dev mongodb" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" From 6d97a535b3145663ccf521006759e4afaa852e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20Rocha?= Date: Wed, 28 Nov 2012 19:02:02 -0500 Subject: [PATCH 051/736] Move APT dependencies and repos to their own files --- apt-packages.txt | 25 +++++++++++++++++++++++++ apt-repos.txt | 3 +++ create-dev-env.sh | 14 +++++++++----- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 apt-packages.txt create mode 100644 apt-repos.txt diff --git a/apt-packages.txt b/apt-packages.txt new file mode 100644 index 0000000000..b783ccb67e --- /dev/null +++ b/apt-packages.txt @@ -0,0 +1,25 @@ +python-software-properties +pkg-config +curl +git +python-virtualenv +build-essential +python-dev +gfortran +liblapack-dev +libfreetype6-dev +libpng12-dev +libxml2-dev +libxslt-dev +yui-compressor +graphviz +graphviz-dev +mysql-server +libmysqlclient-dev +libgeos-dev +libreadline6 +libreadline6-dev +mongodb +nodejs +npm +coffeescript diff --git a/apt-repos.txt b/apt-repos.txt new file mode 100644 index 0000000000..6ce9f2c34b --- /dev/null +++ b/apt-repos.txt @@ -0,0 +1,3 @@ +ppa:chris-lea/node.js +ppa:chris-lea/node.js-libs +ppa:chris-lea/libjs-underscore diff --git a/create-dev-env.sh b/create-dev-env.sh index c30f0f3732..12ee163043 100755 --- a/create-dev-env.sh +++ b/create-dev-env.sh @@ -98,8 +98,9 @@ RUBY_VER="1.9.3" NUMPY_VER="1.6.2" SCIPY_VER="0.10.1" BREW_FILE="$BASE/mitx/brew-formulas.txt" +APT_REPOS_FILE="$BASE/mitx/apt-repos.txt" +APT_PKGS_FILE="$BASE/mitx/apt-packages.txt" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" -APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor nodejs npm graphviz graphviz-dev mysql-server libmysqlclient-dev libgeos-dev coffeescript libreadline6 libreadline6-dev mongodb" if [[ $EUID -eq 0 ]]; then error "This script should not be run using sudo or as the root user" @@ -188,12 +189,15 @@ case `uname -s` in maya|lisa|natty|oneiric|precise|quantal) output "Installing ubuntu requirements" - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:chris-lea/node.js + # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation + export DEBIAN_FRONTEND=noninteractive + + # add repositories + cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y sudo apt-get -y update - # DEBIAN_FRONTEND=noninteractive is required for silent mysql-server installation - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install $APT_PKGS + # install packages listed in APT_PKGS_FILE + cat $APT_PKGS_FILE | xargs sudo apt-get -y install clone_repos ;; From f0b4ac3ab8183ff2af39595bffa87e29c2dc61cd Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 14:11:29 -0500 Subject: [PATCH 052/736] add open ended input type and html template --- .idea/.name | 1 + .idea/encodings.xml | 5 + .../inspectionProfiles/profiles_settings.xml | 7 + .idea/misc.xml | 8 + .idea/mitx.iml | 9 + .idea/modules.xml | 9 + .idea/scopes/scope_settings.xml | 5 + .idea/vcs.xml | 7 + .idea/workspace.xml | 516 ++++++++++++++++++ common/lib/capa/capa/inputtypes.py | 49 ++ .../capa/capa/templates/openendedinput.html | 32 ++ 11 files changed, 648 insertions(+) create mode 100644 .idea/.name create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/mitx.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 common/lib/capa/capa/templates/openendedinput.html diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000000..36d4bf6d51 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +mitx \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000..e206d70d85 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000..c60c33bb47 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..e746fb7776 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + $APPLICATION_HOME_DIR$/lib/pycharm.jar!/resources/html5-schema/html5.rnc + + + + diff --git a/.idea/mitx.iml b/.idea/mitx.iml new file mode 100644 index 0000000000..2d50e7b1fc --- /dev/null +++ b/.idea/mitx.iml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..7eaf1301cf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000000..922003b843 --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..c80f2198b5 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000000..ef9844844f --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1351640399572 + 1351640399572 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index ec1cda83c7..70fe5dd6c8 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -733,3 +733,52 @@ class ChemicalEquationInput(InputTypeBase): return {'previewer': '/static/js/capa/chemical_equation_preview.js',} registry.register(ChemicalEquationInput) + +#----------------------------------------------------------------------------- + +class OpenEndedInput(InputTypeBase): + """ + A text area input for code--uses codemirror, does syntax highlighting, special tab handling, + etc. + """ + + template = "openendedinput.html" + tags = ['openendedinput'] + + # pulled out for testing + submitted_msg = ("Submitted. As soon as your submission is" + " graded, this message will be replaced with the grader's feedback.") + + @classmethod + def get_attributes(cls): + """ + Convert options to a convenient format. + """ + return [Attribute('rows', '30'), + Attribute('cols', '80'), + Attribute('hidden', ''), + ] + + def setup(self): + """ + Implement special logic: handle queueing state, and default input. + """ + # if no student input yet, then use the default input given by the problem + if not self.value: + self.value = self.xml.text + + # Check if problem has been queued + self.queue_len = 0 + # Flag indicating that the problem has been queued, 'msg' is length of queue + if self.status == 'incomplete': + self.status = 'queued' + self.queue_len = self.msg + self.msg = self.submitted_msg + + def _extra_context(self): + """Defined queue_len, add it """ + return {'queue_len': self.queue_len,} + +registry.register(OpenEndedInput) + +#----------------------------------------------------------------------------- \ No newline at end of file diff --git a/common/lib/capa/capa/templates/openendedinput.html b/common/lib/capa/capa/templates/openendedinput.html new file mode 100644 index 0000000000..697bff8082 --- /dev/null +++ b/common/lib/capa/capa/templates/openendedinput.html @@ -0,0 +1,32 @@ +
+ + +
+ % if status == 'unsubmitted': + Unanswered + % elif status == 'correct': + Correct + % elif status == 'incorrect': + Incorrect + % elif status == 'queued': + Queued + + % endif + + % if hidden: +
+ % endif + +

${status}

+
+ + + +
+ ${msg|n} +
+
From 329ea7ab7200db8662767758b95cfd9aa3378a6a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 14:12:22 -0500 Subject: [PATCH 053/736] removed .idea files --- .gitmodules | 0 .idea/.name | 1 - .idea/encodings.xml | 5 - .../inspectionProfiles/profiles_settings.xml | 7 - .idea/misc.xml | 8 - .idea/mitx.iml | 9 - .idea/modules.xml | 9 - .idea/scopes/scope_settings.xml | 5 - .idea/vcs.xml | 7 - .idea/workspace.xml | 516 ------------------ lms/static/admin/css/ie.css | 63 --- 11 files changed, 630 deletions(-) delete mode 100644 .gitmodules delete mode 100644 .idea/.name delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/mitx.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/scopes/scope_settings.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml delete mode 100644 lms/static/admin/css/ie.css diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 36d4bf6d51..0000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -mitx \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index e206d70d85..0000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index c60c33bb47..0000000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index e746fb7776..0000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - $APPLICATION_HOME_DIR$/lib/pycharm.jar!/resources/html5-schema/html5.rnc - - - - diff --git a/.idea/mitx.iml b/.idea/mitx.iml deleted file mode 100644 index 2d50e7b1fc..0000000000 --- a/.idea/mitx.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 7eaf1301cf..0000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b843..0000000000 --- a/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index c80f2198b5..0000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index ef9844844f..0000000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,516 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1351640399572 - 1351640399572 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lms/static/admin/css/ie.css b/lms/static/admin/css/ie.css deleted file mode 100644 index fd00f7f204..0000000000 --- a/lms/static/admin/css/ie.css +++ /dev/null @@ -1,63 +0,0 @@ -/* IE 6 & 7 */ - -/* Proper fixed width for dashboard in IE6 */ - -.dashboard #content { - *width: 768px; -} - -.dashboard #content-main { - *width: 535px; -} - -/* IE 6 ONLY */ - -/* Keep header from flowing off the page */ - -#container { - _position: static; -} - -/* Put the right sidebars back on the page */ - -.colMS #content-related { - _margin-right: 0; - _margin-left: 10px; - _position: static; -} - -/* Put the left sidebars back on the page */ - -.colSM #content-related { - _margin-right: 10px; - _margin-left: -115px; - _position: static; -} - -.form-row { - _height: 1%; -} - -/* Fix right margin for changelist filters in IE6 */ - -#changelist-filter ul { - _margin-right: -10px; -} - -/* IE ignores min-height, but treats height as if it were min-height */ - -.change-list .filtered { - _height: 400px; -} - -/* IE doesn't know alpha transparency in PNGs */ - -.inline-deletelink { - background: transparent url(../img/inline-delete-8bit.png) no-repeat; -} - -/* IE7 doesn't support inline-block */ -.change-list ul.toplinks li { - zoom: 1; - *display: inline; -} \ No newline at end of file From de6e913d983f2f1e7cccf5694f318352b55dc7b1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 14:51:51 -0500 Subject: [PATCH 054/736] copy code response input type and modify a bit --- common/lib/capa/capa/responsetypes.py | 204 +++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 20e7c43577..53c9637373 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1814,6 +1814,207 @@ class ImageResponse(LoncapaResponse): return (dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]), dict([(ie.get('id'), ie.get('regions')) for ie in self.ielements])) #----------------------------------------------------------------------------- + +class OpenEndedResponse(LoncapaResponse): + """ + Grade student open ended responses using an external queueing server, called 'xqueue' + + Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by OpenEndedResponse: + system.xqueue = { 'interface': XqueueInterface object, + 'callback_url': Per-StudentModule callback URL + where results are posted (string), + 'default_queuename': Default queuename to submit request (string) + } + + External requests are only submitted for student submission grading + (i.e. and not for getting reference answers) + """ + + response_tag = 'openendedresponse' + allowed_inputfields = ['openendedinput'] + max_inputfields = 1 + + def setup_response(self): + ''' + Configure OpenEndedResponse from XML. + ''' + xml = self.xml + # TODO: XML can override external resource (grader/queue) URL + self.url = xml.get('url', None) + self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) + + # VS[compat]: + # Check if XML uses the ExternalResponse format or the generic OpenEndedResponse format + oeparam = self.xml.find('openendedparam') + self._parse_openendedresponse_xml(oeparam) + + def _parse_openendedresponse_xml(self,oeparam): + ''' + Parse OpenEndedResponse XML: + self.initial_display + self.answer (an answer to display to the student in the LMS) + self.payload + ''' + # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload + grader_payload = oeparam.find('grader_payload') + grader_payload = grader_payload.text if grader_payload is not None else '' + self.payload = {'grader_payload': grader_payload} + + answer_display = oeparam.find('answer_display') + if answer_display is not None: + self.answer = answer_display.text + else: + self.answer = 'No answer provided.' + + initial_display = oeparam.find('initial_display') + if initial_display is not None: + self.initial_display = initial_display.text + else: + self.initial_display = '' + + def get_score(self, student_answers): + try: + # Note that submission can be a file + submission = student_answers[self.answer_id] + except Exception as err: + log.error('Error in OpenEndedResponse %s: cannot get student answer for %s;' + ' student_answers=%s' % + (err, self.answer_id, convert_files_to_filenames(student_answers))) + raise Exception(err) + + # Prepare xqueue request + #------------------------------------------------------------ + + qinterface = self.system.xqueue['interface'] + qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) + + anonymous_student_id = self.system.anonymous_student_id + + # Generate header + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + + anonymous_student_id + + self.answer_id) + xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], + lms_key=queuekey, + queue_name=self.queue_name) + + self.context.update({'submission': submission}) + + 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, + } + contents.update({'student_info': json.dumps(student_info)}) + + # Submit request. When successful, 'msg' is the prior length of the queue + contents.update({'student_response': submission}) + (error, msg) = qinterface.send_to_queue(header=xheader, + body=json.dumps(contents)) + + # State associated with the queueing request + queuestate = {'key': queuekey, + 'time': qtime,} + + cmap = CorrectMap() + if error: + cmap.set(self.answer_id, queuestate=None, + msg='Unable to deliver your submission to grader. (Reason: %s.)' + ' Please try again later.' % msg) + else: + # Queueing mechanism flags: + # 1) Backend: Non-null CorrectMap['queuestate'] indicates that + # the problem has been queued + # 2) Frontend: correctness='incomplete' eventually trickles down + # through inputtypes.textbox and .filesubmission to inform the + # browser to poll the LMS + cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) + + return cmap + + def update_score(self, score_msg, oldcmap, queuekey): + + (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) + if not valid_score_msg: + oldcmap.set(self.answer_id, + msg='Invalid grader reply. Please contact the course staff.') + return oldcmap + + correctness = 'correct' if correct else 'incorrect' + + # TODO: Find out how this is used elsewhere, if any + self.context['correct'] = correctness + + # Replace 'oldcmap' with new grading results if queuekey matches. If queuekey + # does not match, we keep waiting for the score_msg whose key actually matches + if oldcmap.is_right_queuekey(self.answer_id, queuekey): + # Sanity check on returned points + if points < 0: + points = 0 + elif points > self.maxpoints[self.answer_id]: + points = self.maxpoints[self.answer_id] + # Queuestate is consumed + oldcmap.set(self.answer_id, npoints=points, correctness=correctness, + msg=msg.replace(' ', ' '), queuestate=None) + else: + log.debug('OpenEndedResponse: queuekey %s does not match for answer_id=%s.' % + (queuekey, self.answer_id)) + + return oldcmap + + def get_answers(self): + anshtml = '
%s
' % self.answer + return {self.answer_id: anshtml} + + def get_initial_display(self): + return {self.answer_id: self.initial_display} + + def _parse_score_msg(self, score_msg): + """ + Grader reply is a JSON-dump of the following dict + { 'correct': True/False, + 'score': Numeric value (floating point is okay) to assign to answer + 'msg': grader_msg } + + Returns (valid_score_msg, correct, score, msg): + valid_score_msg: Flag indicating valid score_msg format (Boolean) + correct: Correctness of submission (Boolean) + score: Points to be assigned (numeric, can be float) + msg: Message from grader to display to student (string) + """ + fail = (False, False, 0, '') + try: + score_result = json.loads(score_msg) + except (TypeError, ValueError): + log.error("External grader message should be a JSON-serialized dict." + " Received score_msg = %s" % score_msg) + return fail + if not isinstance(score_result, dict): + log.error("External grader message should be a JSON-serialized dict." + " Received score_result = %s" % score_result) + return fail + for tag in ['correct', 'score', 'msg']: + if tag not in score_result: + log.error("External grader message is missing one or more required" + " tags: 'correct', 'score', 'msg'") + return fail + + # Next, we need to check that the contents of the external grader message + # is safe for the LMS. + # 1) Make sure that the message is valid XML (proper opening/closing tags) + # 2) TODO: Is the message actually HTML? + msg = score_result['msg'] + try: + etree.fromstring(msg) + except etree.XMLSyntaxError as err: + log.error("Unable to parse external grader message as valid" + " XML: score_msg['msg']=%s" % msg) + return fail + + return (True, score_result['correct'], score_result['score'], msg) + +#----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration @@ -1830,4 +2031,5 @@ __all__ = [CodeResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, - JavascriptResponse] + JavascriptResponse, + OpenEndedResponse] From 8e87d49228c20514f83ddb661034eebb4da240f6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 15:30:34 -0500 Subject: [PATCH 055/736] setup grader type parameter to be paarsed from xml --- common/lib/capa/capa/responsetypes.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 53c9637373..d5b0c86ec1 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1852,26 +1852,27 @@ class OpenEndedResponse(LoncapaResponse): ''' Parse OpenEndedResponse XML: self.initial_display - self.answer (an answer to display to the student in the LMS) self.payload + self.grader_type - type of grader to use. One of 'peer','ml','turk' + ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' self.payload = {'grader_payload': grader_payload} - answer_display = oeparam.find('answer_display') - if answer_display is not None: - self.answer = answer_display.text - else: - self.answer = 'No answer provided.' - initial_display = oeparam.find('initial_display') if initial_display is not None: self.initial_display = initial_display.text else: self.initial_display = '' + grader_type = oeparam.find('initial_display') + if grader_type is not None: + self.grader_type = grader_type.text + else: + self.grader_type='ml' + def get_score(self, student_answers): try: # Note that submission can be a file @@ -1910,6 +1911,7 @@ class OpenEndedResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue contents.update({'student_response': submission}) + contents.update({'grader_type'} : self.grader_type) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) From 79941271d7341a0adde11f8c708c2987bdda61b8 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 15:50:18 -0500 Subject: [PATCH 056/736] fix bracket in wrong spot --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d5b0c86ec1..e18c894452 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1911,7 +1911,7 @@ class OpenEndedResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue contents.update({'student_response': submission}) - contents.update({'grader_type'} : self.grader_type) + contents.update({'grader_type' : self.grader_type}) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) From 5c7e6415be911a3007d527a952fe354f66847777 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Mon, 5 Nov 2012 18:10:29 -0500 Subject: [PATCH 057/736] Modify capa types --- common/lib/capa/capa/responsetypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index e18c894452..6fd975aa45 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1867,12 +1867,14 @@ class OpenEndedResponse(LoncapaResponse): else: self.initial_display = '' - grader_type = oeparam.find('initial_display') + grader_type = oeparam.find('grader_type') if grader_type is not None: self.grader_type = grader_type.text else: self.grader_type='ml' + self.answer="None available." + def get_score(self, student_answers): try: # Note that submission can be a file @@ -1912,6 +1914,7 @@ class OpenEndedResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue contents.update({'student_response': submission}) contents.update({'grader_type' : self.grader_type}) + log.debug(contents) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) From bf361af3b37c351933142a71d496912e76f99920 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 6 Nov 2012 20:43:38 -0500 Subject: [PATCH 058/736] Added debug code, then removed it --- common/lib/capa/capa/responsetypes.py | 5 +---- common/lib/capa/capa/xqueue_interface.py | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6fd975aa45..d12e451b7a 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1843,8 +1843,6 @@ class OpenEndedResponse(LoncapaResponse): self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) - # VS[compat]: - # Check if XML uses the ExternalResponse format or the generic OpenEndedResponse format oeparam = self.xml.find('openendedparam') self._parse_openendedresponse_xml(oeparam) @@ -1914,7 +1912,7 @@ class OpenEndedResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue contents.update({'student_response': submission}) contents.update({'grader_type' : self.grader_type}) - log.debug(contents) + (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) @@ -1939,7 +1937,6 @@ class OpenEndedResponse(LoncapaResponse): return cmap def update_score(self, score_msg, oldcmap, queuekey): - (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: oldcmap.set(self.answer_id, diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index 0214488cce..f145cad23c 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -49,6 +49,7 @@ def parse_xreply(xreply): return_code = xreply['return_code'] content = xreply['content'] + return (return_code, content) From e2d19d1a4083f57e587d51889635b95fdb82e02e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 10:21:56 -0500 Subject: [PATCH 059/736] Add openendedparam to list of tags to remove from output html --- common/lib/capa/capa/capa_problem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index db42fb698a..53ddab00f3 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -53,7 +53,7 @@ response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__]) solution_tags = ['solution'] # these get captured as student responses -response_properties = ["codeparam", "responseparam", "answer"] +response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] # special problem tags which should be turned into innocuous HTML html_transforms = {'problem': {'tag': 'div'}, @@ -72,7 +72,7 @@ global_context = {'random': random, 'miller': chem.miller} # These should be removed from HTML output, including all subelements -html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] +html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam"] log = logging.getLogger('mitx.' + __name__) From ce7444780bf5e9528fa3244957ed80b9cbfe7b00 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 11:53:01 -0500 Subject: [PATCH 060/736] Add in feedback, work on css to display open ended feedback --- common/lib/capa/capa/responsetypes.py | 10 +++++++--- common/lib/xmodule/xmodule/css/capa/display.scss | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d12e451b7a..4eaaa1607f 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1937,6 +1937,7 @@ class OpenEndedResponse(LoncapaResponse): return cmap def update_score(self, score_msg, oldcmap, queuekey): + log.debug(score_msg) (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: oldcmap.set(self.answer_id, @@ -1996,17 +1997,20 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['correct', 'score', 'msg']: + for tag in ['correct', 'score', 'msg', 'feedback']: if tag not in score_result: log.error("External grader message is missing one or more required" - " tags: 'correct', 'score', 'msg'") + " tags: 'correct', 'score', 'msg', 'feedback") return fail + #Extract feedback from score_result + feedback = score_result['feedback'] # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? msg = score_result['msg'] + try: etree.fromstring(msg) except etree.XMLSyntaxError as err: @@ -2014,7 +2018,7 @@ class OpenEndedResponse(LoncapaResponse): " XML: score_msg['msg']=%s" % msg) return fail - return (True, score_result['correct'], score_result['score'], msg) + return (True, score_result['correct'], score_result['score'], feedback) #----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index fd67a3804e..098100f297 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -685,6 +685,21 @@ section.problem { color: #B00; } } + + .markup-text{ + margin: 5px; + padding: 20px 0px 15px 50px; + border-top: 1px solid #DDD; + border-left: 20px solid #FAFAFA; + + .badspelling { + color: #BB0000; + } + + .badgrammar { + color: #BDA046; + } + } } } } From d59681e3aa4ddfd584648020ff6ef1c66aba4ddc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 13:52:04 -0500 Subject: [PATCH 061/736] Alter css tag names, change documentation and answer display settings. --- common/lib/capa/capa/responsetypes.py | 19 ++++++++++--------- .../lib/xmodule/xmodule/css/capa/display.scss | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 4eaaa1607f..bdeea036f6 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1850,28 +1850,29 @@ class OpenEndedResponse(LoncapaResponse): ''' Parse OpenEndedResponse XML: self.initial_display - self.payload - self.grader_type - type of grader to use. One of 'peer','ml','turk' - + self.payload - dict containing keys -- + 'grader' : path to grader settings file, 'problem_id' : id of the problem + + self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' self.payload = {'grader_payload': grader_payload} + #Parse initial display initial_display = oeparam.find('initial_display') if initial_display is not None: self.initial_display = initial_display.text else: self.initial_display = '' - grader_type = oeparam.find('grader_type') - if grader_type is not None: - self.grader_type = grader_type.text + #Parse answer display + answer_display = oeparam.find('answer_display') + if answer_display is not None: + self.answer= answer_display.text else: - self.grader_type='ml' - - self.answer="None available." + self.answer = "No answer available." def get_score(self, student_answers): try: diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 098100f297..58ba7b00ed 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -692,11 +692,11 @@ section.problem { border-top: 1px solid #DDD; border-left: 20px solid #FAFAFA; - .badspelling { + bs { color: #BB0000; } - .badgrammar { + bg { color: #BDA046; } } From a901757dd741dc82fcce39c7bf2ac3338390cec7 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 13:53:45 -0500 Subject: [PATCH 062/736] Fix minor bug with removed grader type tag --- common/lib/capa/capa/responsetypes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bdeea036f6..7101dccfd9 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1852,7 +1852,7 @@ class OpenEndedResponse(LoncapaResponse): self.initial_display self.payload - dict containing keys -- 'grader' : path to grader settings file, 'problem_id' : id of the problem - + self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload @@ -1912,7 +1912,6 @@ class OpenEndedResponse(LoncapaResponse): # Submit request. When successful, 'msg' is the prior length of the queue contents.update({'student_response': submission}) - contents.update({'grader_type' : self.grader_type}) (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) From dba8a7a767a477336d06589386a5d22e007c1d63 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 14:16:56 -0500 Subject: [PATCH 063/736] Add anonymous student id to grader payload --- common/lib/capa/capa/responsetypes.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 7101dccfd9..bef79e3a4b 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1843,6 +1843,7 @@ class OpenEndedResponse(LoncapaResponse): self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) + #Look for tag named openendedparam that encapsulates all grader settings oeparam = self.xml.find('openendedparam') self._parse_openendedresponse_xml(oeparam) @@ -1858,8 +1859,17 @@ class OpenEndedResponse(LoncapaResponse): # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' + + #Update grader payload with student id. If grader payload not json, error. + try: + grader_payload=json.loads(grader_payload) + grader_payload.update({'student_id' : self.system.anonymous_student_id}) + grader_payload=json.dumps(grader_payload) + except Exception as err: + log.error("Grader payload is not a json object!") self.payload = {'grader_payload': grader_payload} + #Parse initial display initial_display = oeparam.find('initial_display') if initial_display is not None: From 941ef528638315b2316e654e961b0882df921749 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 7 Nov 2012 15:54:49 -0500 Subject: [PATCH 064/736] Add docs, fix some logic --- common/lib/capa/capa/responsetypes.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bef79e3a4b..daaea90562 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1867,8 +1867,8 @@ class OpenEndedResponse(LoncapaResponse): grader_payload=json.dumps(grader_payload) except Exception as err: log.error("Grader payload is not a json object!") - self.payload = {'grader_payload': grader_payload} + self.payload = {'grader_payload': grader_payload} #Parse initial display initial_display = oeparam.find('initial_display') @@ -1882,16 +1882,15 @@ class OpenEndedResponse(LoncapaResponse): if answer_display is not None: self.answer= answer_display.text else: - self.answer = "No answer available." + self.answer = "No answer given." def get_score(self, student_answers): + try: - # Note that submission can be a file submission = student_answers[self.answer_id] except Exception as err: log.error('Error in OpenEndedResponse %s: cannot get student answer for %s;' - ' student_answers=%s' % - (err, self.answer_id, convert_files_to_filenames(student_answers))) + ' student_answers=%s', err, self.answer_id) raise Exception(err) # Prepare xqueue request @@ -1906,6 +1905,7 @@ class OpenEndedResponse(LoncapaResponse): queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + anonymous_student_id + self.answer_id) + xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], lms_key=queuekey, queue_name=self.queue_name) @@ -1918,11 +1918,11 @@ class OpenEndedResponse(LoncapaResponse): student_info = {'anonymous_student_id': anonymous_student_id, 'submission_time': qtime, } - contents.update({'student_info': json.dumps(student_info)}) + + #Update contents with student response and student info + contents.update({'student_info': json.dumps(student_info), 'student_response': submission}) # Submit request. When successful, 'msg' is the prior length of the queue - contents.update({'student_response': submission}) - (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) @@ -1988,7 +1988,9 @@ class OpenEndedResponse(LoncapaResponse): Grader reply is a JSON-dump of the following dict { 'correct': True/False, 'score': Numeric value (floating point is okay) to assign to answer - 'msg': grader_msg } + 'msg': grader_msg + 'feedback' : feedback from grader + } Returns (valid_score_msg, correct, score, msg): valid_score_msg: Flag indicating valid score_msg format (Boolean) @@ -2012,22 +2014,24 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message is missing one or more required" " tags: 'correct', 'score', 'msg', 'feedback") return fail - #Extract feedback from score_result - feedback = score_result['feedback'] # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? msg = score_result['msg'] + feedback = score_result['feedback'] try: etree.fromstring(msg) + etree.fromstring(feedback) except etree.XMLSyntaxError as err: log.error("Unable to parse external grader message as valid" - " XML: score_msg['msg']=%s" % msg) + " Msg: score_msg['msg']=%r " + "\n Feedback : score_result['feedback'] = %r", msg, feedback) return fail + #Currently ignore msg and only return feedback (which takes the place of msg) return (True, score_result['correct'], score_result['score'], feedback) #----------------------------------------------------------------------------- From 06a00a982ae301ff9dde498100c5344c2378546d Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 13 Nov 2012 11:56:14 -0500 Subject: [PATCH 065/736] Send location to external grader --- common/lib/capa/capa/responsetypes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index daaea90562..3193643318 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1863,7 +1863,8 @@ class OpenEndedResponse(LoncapaResponse): #Update grader payload with student id. If grader payload not json, error. try: grader_payload=json.loads(grader_payload) - grader_payload.update({'student_id' : self.system.anonymous_student_id}) + location=self.system.ajax_url.split("://")[1] + grader_payload.update({'location' : location}) grader_payload=json.dumps(grader_payload) except Exception as err: log.error("Grader payload is not a json object!") From 9abd80203fba176c654876bceb2be991e9042571 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Wed, 14 Nov 2012 10:24:19 -0500 Subject: [PATCH 066/736] Pass course id in payload --- common/lib/capa/capa/responsetypes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 3193643318..dc120f7f47 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1864,7 +1864,11 @@ class OpenEndedResponse(LoncapaResponse): try: grader_payload=json.loads(grader_payload) location=self.system.ajax_url.split("://")[1] - grader_payload.update({'location' : location}) + org,course,type,name=location.split("/") + grader_payload.update({ + 'location' : location, + 'course_id' : "{0}/{1}".format(org,course) + }) grader_payload=json.dumps(grader_payload) except Exception as err: log.error("Grader payload is not a json object!") From 14403103a440cca4635b51b31e6d0bf7cf3c33e6 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Nov 2012 14:00:45 -0500 Subject: [PATCH 067/736] Remove msg key from xqueue passback dict --- common/lib/capa/capa/responsetypes.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index dc120f7f47..0a5471d47f 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2001,7 +2001,6 @@ class OpenEndedResponse(LoncapaResponse): valid_score_msg: Flag indicating valid score_msg format (Boolean) correct: Correctness of submission (Boolean) score: Points to be assigned (numeric, can be float) - msg: Message from grader to display to student (string) """ fail = (False, False, 0, '') try: @@ -2014,26 +2013,23 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['correct', 'score', 'msg', 'feedback']: + for tag in ['correct', 'score','feedback']: if tag not in score_result: log.error("External grader message is missing one or more required" - " tags: 'correct', 'score', 'msg', 'feedback") + " tags: 'correct', 'score', 'feedback") return fail # Next, we need to check that the contents of the external grader message # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? - msg = score_result['msg'] feedback = score_result['feedback'] try: - etree.fromstring(msg) etree.fromstring(feedback) except etree.XMLSyntaxError as err: log.error("Unable to parse external grader message as valid" - " Msg: score_msg['msg']=%r " - "\n Feedback : score_result['feedback'] = %r", msg, feedback) + "\n Feedback : score_result['feedback'] = %r",feedback) return fail #Currently ignore msg and only return feedback (which takes the place of msg) From 0c69c466583f17429d0b2cc4243cf00df15b6f2c Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Nov 2012 15:55:00 -0500 Subject: [PATCH 068/736] Remove need for msg --- common/lib/capa/capa/responsetypes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 0a5471d47f..6cf04458b6 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1959,6 +1959,7 @@ class OpenEndedResponse(LoncapaResponse): msg='Invalid grader reply. Please contact the course staff.') return oldcmap + correctness = 'correct' if correct else 'incorrect' # TODO: Find out how this is used elsewhere, if any @@ -2025,6 +2026,10 @@ class OpenEndedResponse(LoncapaResponse): # 2) TODO: Is the message actually HTML? feedback = score_result['feedback'] + correct=False + if score_result['correct']=="True": + correct=True + try: etree.fromstring(feedback) except etree.XMLSyntaxError as err: @@ -2033,7 +2038,7 @@ class OpenEndedResponse(LoncapaResponse): return fail #Currently ignore msg and only return feedback (which takes the place of msg) - return (True, score_result['correct'], score_result['score'], feedback) + return (True, correct, score_result['score'], feedback) #----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses From e17db85d34b5db91b5f3fc34c93754e3b0bbaa6a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Thu, 15 Nov 2012 16:11:44 -0500 Subject: [PATCH 069/736] Add in max score attribute for proper instructor scoring --- common/lib/capa/capa/responsetypes.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6cf04458b6..887aaa9752 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1889,6 +1889,18 @@ class OpenEndedResponse(LoncapaResponse): else: self.answer = "No answer given." + #Parse max_score + top_score = oeparam.find('max_score') + if top_score is not None: + try: + self.max_score= int(top_score.text) + except: + self.top_score=1 + else: + self.max_score = 1 + + log.debug(self.max_score) + def get_score(self, student_answers): try: @@ -1925,7 +1937,11 @@ class OpenEndedResponse(LoncapaResponse): } #Update contents with student response and student info - contents.update({'student_info': json.dumps(student_info), 'student_response': submission}) + contents.update({ + 'student_info': json.dumps(student_info), + 'student_response': submission, + 'max_score' : self.max_score + }) # Submit request. When successful, 'msg' is the prior length of the queue (error, msg) = qinterface.send_to_queue(header=xheader, From 565f502cb17707d64c1c5c86eed3218020464db4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 09:28:10 -0500 Subject: [PATCH 070/736] Add in prompt tag to openended response and parse/pass along properly --- common/lib/capa/capa/responsetypes.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 887aaa9752..1fa93eac62 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1845,9 +1845,10 @@ class OpenEndedResponse(LoncapaResponse): #Look for tag named openendedparam that encapsulates all grader settings oeparam = self.xml.find('openendedparam') - self._parse_openendedresponse_xml(oeparam) + prompt=self.xml.find('prompt') + self._parse_openendedresponse_xml(oeparam,prompt) - def _parse_openendedresponse_xml(self,oeparam): + def _parse_openendedresponse_xml(self,oeparam,prompt): ''' Parse OpenEndedResponse XML: self.initial_display @@ -1857,6 +1858,16 @@ class OpenEndedResponse(LoncapaResponse): self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload + + #Modify code from stringify_children in xmodule. Didn't import directly in order to avoid capa depending + #on xmodule (seems to be avoided in code) + prompt_parts=[prompt.text] + [prompt_parts.append((etree.tostring(p, with_tail=True))) for p in prompt.getchildren()] + prompt_string=' '.join(prompt_parts) + + #Strip html tags from prompt. This may need to be removed in order to display prompt to instructors properly. + prompt_string=re.sub('<[^<]+?>', '', prompt_string) + grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -1867,7 +1878,8 @@ class OpenEndedResponse(LoncapaResponse): org,course,type,name=location.split("/") grader_payload.update({ 'location' : location, - 'course_id' : "{0}/{1}".format(org,course) + 'course_id' : "{0}/{1}".format(org,course), + 'prompt' : prompt_string }) grader_payload=json.dumps(grader_payload) except Exception as err: From d71445f9d89caf9f3f84396b02a82b8cfebdd8b1 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 13:52:58 -0500 Subject: [PATCH 071/736] Add rubric field to open ended response --- common/lib/capa/capa/responsetypes.py | 32 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 1fa93eac62..14b3c97886 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1846,9 +1846,25 @@ class OpenEndedResponse(LoncapaResponse): #Look for tag named openendedparam that encapsulates all grader settings oeparam = self.xml.find('openendedparam') prompt=self.xml.find('prompt') + rubric=self.xml.find('rubric') self._parse_openendedresponse_xml(oeparam,prompt) - def _parse_openendedresponse_xml(self,oeparam,prompt): + def stringify_children(self,node,strip_tags=True): + """ + Modify code from stringify_children in xmodule. Didn't import directly in order to avoid capa depending + on xmodule (seems to be avoided in code) + """ + parts=[node.text] + [parts.append((etree.tostring(p, with_tail=True))) for p in node.getchildren()] + node_string=' '.join(parts) + + #Strip html tags from prompt. This may need to be removed in order to display prompt to instructors properly. + if strip_tags: + node_string=re.sub('<[^<]+?>', '', node_string) + + return node_string + + def _parse_openendedresponse_xml(self,oeparam,prompt,rubric): ''' Parse OpenEndedResponse XML: self.initial_display @@ -1858,15 +1874,8 @@ class OpenEndedResponse(LoncapaResponse): self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload - - #Modify code from stringify_children in xmodule. Didn't import directly in order to avoid capa depending - #on xmodule (seems to be avoided in code) - prompt_parts=[prompt.text] - [prompt_parts.append((etree.tostring(p, with_tail=True))) for p in prompt.getchildren()] - prompt_string=' '.join(prompt_parts) - - #Strip html tags from prompt. This may need to be removed in order to display prompt to instructors properly. - prompt_string=re.sub('<[^<]+?>', '', prompt_string) + prompt_string=self.stringify_children(prompt) + rubric_string=self.stringify_children(rubric) grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' @@ -1879,7 +1888,8 @@ class OpenEndedResponse(LoncapaResponse): grader_payload.update({ 'location' : location, 'course_id' : "{0}/{1}".format(org,course), - 'prompt' : prompt_string + 'prompt' : prompt_string, + 'rubric' : rubric_string, }) grader_payload=json.dumps(grader_payload) except Exception as err: From 1e194f0e1aa0cb159eccafedee7b8a638caef4c0 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 13:57:46 -0500 Subject: [PATCH 072/736] Fix rubric code, make sure it is removed properly. --- common/lib/capa/capa/capa_problem.py | 2 +- common/lib/capa/capa/responsetypes.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 53ddab00f3..2eaa0e4286 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -72,7 +72,7 @@ global_context = {'random': random, 'miller': chem.miller} # These should be removed from HTML output, including all subelements -html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam"] +html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam","openendedrubric"] log = logging.getLogger('mitx.' + __name__) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 14b3c97886..82ed4d2ff7 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1846,8 +1846,8 @@ class OpenEndedResponse(LoncapaResponse): #Look for tag named openendedparam that encapsulates all grader settings oeparam = self.xml.find('openendedparam') prompt=self.xml.find('prompt') - rubric=self.xml.find('rubric') - self._parse_openendedresponse_xml(oeparam,prompt) + rubric=self.xml.find('openendedrubric') + self._parse_openendedresponse_xml(oeparam,prompt,rubric) def stringify_children(self,node,strip_tags=True): """ From 422ecb5b9d1e22b4e0ae7ff95255a8fb62041904 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 18:51:56 -0500 Subject: [PATCH 073/736] define correctness in the response type --- common/lib/capa/capa/responsetypes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 82ed4d2ff7..6a9ac98171 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2052,7 +2052,7 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['correct', 'score','feedback']: + for tag in ['score','feedback']: if tag not in score_result: log.error("External grader message is missing one or more required" " tags: 'correct', 'score', 'feedback") @@ -2064,8 +2064,10 @@ class OpenEndedResponse(LoncapaResponse): # 2) TODO: Is the message actually HTML? feedback = score_result['feedback'] - correct=False - if score_result['correct']=="True": + score_ratio=int(score_result['score'])/self.max_score + + correct=FALSE + if score_ratio>=.66: correct=True try: From 1a22d3a15fb942b3dd9c0675b91c98f41d1087ef Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 19:04:46 -0500 Subject: [PATCH 074/736] Lowercase False --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 6a9ac98171..31fb7b0f0f 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2066,7 +2066,7 @@ class OpenEndedResponse(LoncapaResponse): score_ratio=int(score_result['score'])/self.max_score - correct=FALSE + correct=False if score_ratio>=.66: correct=True From c2bf0689b658f6a87a1b68b1ea82e64a6ce76e22 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Tue, 20 Nov 2012 19:24:39 -0500 Subject: [PATCH 075/736] Remove correct from expected tags --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 31fb7b0f0f..ba9f03549e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2055,7 +2055,7 @@ class OpenEndedResponse(LoncapaResponse): for tag in ['score','feedback']: if tag not in score_result: log.error("External grader message is missing one or more required" - " tags: 'correct', 'score', 'feedback") + " tags: 'score', 'feedback") return fail # Next, we need to check that the contents of the external grader message From 6b5125c4dfa1d143d2b66dfd54e94b474c003773 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 13 Nov 2012 18:07:59 -0500 Subject: [PATCH 076/736] fix typos, logger configs --- common/lib/xmodule/xmodule/seq_module.py | 2 +- lms/djangoapps/courseware/tabs.py | 2 +- lms/djangoapps/instructor/views.py | 2 +- lms/urls.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index b625646e66..817ed9ab2e 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -10,7 +10,7 @@ from xmodule.progress import Progress from xmodule.exceptions import NotFoundError from pkg_resources import resource_string -log = logging.getLogger("mitx.common.lib.seq_module") +log = logging.getLogger(__name__) # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 980fedb947..4eaef20089 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -36,7 +36,7 @@ CourseTab = namedtuple('CourseTab', 'name link is_active') # wrong. (e.g. "is there a 'name' field?). Validators can assume # that the type field is valid. # -# - a function that takes a config, a user, and a course, and active_page and +# - a function that takes a config, a user, and a course, an active_page and # return a list of CourseTabs. (e.g. "return a CourseTab with specified # name, link to courseware, and is_active=True/False"). The function can # assume that it is only called with configs of the appropriate type that diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f985cc43a0..fec7536151 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -27,7 +27,7 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -log = logging.getLogger("mitx.courseware") +log = logging.getLogger(__name__) template_imports = {'urllib': urllib} diff --git a/lms/urls.py b/lms/urls.py index 529396c20e..62842ec5fe 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -161,7 +161,7 @@ if settings.COURSEWARE_ENABLED: # input types system so that previews can be context-specific. # Unfortunately, we don't have time to think through the right way to do # that (and implement it), and it's not a terrible thing to provide a - # generic chemican-equation rendering service. + # generic chemical-equation rendering service. url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc', name='preview_chemcalc'), From f5d9e963cc897e19ceead213e7a6dedc0d9dc9af Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 13 Nov 2012 18:46:21 -0500 Subject: [PATCH 077/736] Initial wiring of a staff grading tab. - no actual functionality, but have a tab that renders static html via a view --- lms/djangoapps/courseware/tabs.py | 9 ++++++++ lms/djangoapps/instructor/grading.py | 25 +++++++++++++++++++++ lms/djangoapps/instructor/views.py | 21 +++++++++++++++++ lms/templates/instructor/staff_grading.html | 20 +++++++++++++++++ lms/urls.py | 2 ++ 5 files changed, 77 insertions(+) create mode 100644 lms/djangoapps/instructor/grading.py create mode 100644 lms/templates/instructor/staff_grading.html diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py index 4eaef20089..45b4e1821c 100644 --- a/lms/djangoapps/courseware/tabs.py +++ b/lms/djangoapps/courseware/tabs.py @@ -97,6 +97,14 @@ def _textbooks(tab, user, course, active_page): for index, textbook in enumerate(course.textbooks)] return [] + +def _staff_grading(tab, user, course, active_page): + if has_access(user, course, 'staff'): + link = reverse('staff_grading', args=[course.id]) + return [CourseTab('Staff grading', link, active_page == "staff_grading")] + return [] + + #### Validators @@ -132,6 +140,7 @@ VALID_TAB_TYPES = { 'textbooks': TabImpl(null_validator, _textbooks), 'progress': TabImpl(need_name, _progress), 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), + 'staff_grading': TabImpl(null_validator, _staff_grading), } diff --git a/lms/djangoapps/instructor/grading.py b/lms/djangoapps/instructor/grading.py new file mode 100644 index 0000000000..7a48b25a49 --- /dev/null +++ b/lms/djangoapps/instructor/grading.py @@ -0,0 +1,25 @@ +""" +LMS part of instructor grading: + +- views + ajax handling +- calls the instructor grading service +""" + +import json +import logging + +log = logging.getLogger(__name__) + + +class StaffGrading(object): + """ + Wrap up functionality for staff grading of submissions--interface exposes get_html, ajax views. + """ + def __init__(self, course): + self.course = course + + def get_html(self): + return "Instructor grading!" + # context = {} + # return render_to_string('courseware/instructor_grading_view.html', context) + diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index fec7536151..8a33f1d60b 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -27,6 +27,9 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views +from .grading import StaffGrading + + log = logging.getLogger(__name__) template_imports = {'urllib': urllib} @@ -409,6 +412,24 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, return datatable + +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def staff_grading(request, course_id): + """ + Show the instructor grading interface. + """ + course = get_course_with_access(request.user, course_id, 'staff') + + grading = StaffGrading(course) + + return render_to_response('instructor/staff_grading.html', { + 'view_html': grading.get_html(), + 'course': course, + 'course_id': course_id, + # Checked above + 'staff_access': True, }) + + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): """ diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html new file mode 100644 index 0000000000..b00fd935aa --- /dev/null +++ b/lms/templates/instructor/staff_grading.html @@ -0,0 +1,20 @@ +<%inherit file="/main.html" /> +<%block name="bodyclass">${course.css_class} +<%namespace name='static' file='/static_content.html'/> + +<%block name="headextra"> + <%static:css group='course'/> + + +<%block name="title">${course.number} Staff Grading + +<%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> + +<%block name="js_extra"> + + +
+
+ ${view_html} +
+
diff --git a/lms/urls.py b/lms/urls.py index 62842ec5fe..7c1ccb6bde 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -230,6 +230,8 @@ if settings.COURSEWARE_ENABLED: 'instructor.views.grade_summary', name='grade_summary'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/enroll_students$', 'instructor.views.enroll_students', name='enroll_students'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading$', + 'instructor.views.staff_grading', name='staff_grading'), ) # discussion forums live within courseware, so courseware must be enabled first From 7d1d135c1638d752ad5a79db2a1fa4a2ba9ed32e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 13 Nov 2012 18:46:38 -0500 Subject: [PATCH 078/736] Wire in some initial js. --- lms/envs/common.py | 15 +++++++++++++-- .../coffee/src/staff_grading/staff_grading.coffee | 5 +++++ lms/templates/instructor/staff_grading.html | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 lms/static/coffee/src/staff_grading/staff_grading.coffee diff --git a/lms/envs/common.py b/lms/envs/common.py index dd9013bcb3..008de7ac84 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -406,6 +406,9 @@ main_vendor_js = [ discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee')) +staff_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/staff_grading/**/*.coffee')) + + # Load javascript from all of the available xmodules, and # prep it for use in pipeline js from xmodule.x_module import XModuleDescriptor @@ -468,7 +471,8 @@ with open(module_styles_path, 'w') as module_styles: PIPELINE_JS = { 'application': { - # Application will contain all paths not in courseware_only_js + # Application will contain all paths not in courseware_only_js or + # discussion_js or staff_grading_js 'source_filenames': [ pth.replace(COMMON_ROOT / 'static/', '') for pth @@ -476,7 +480,9 @@ PIPELINE_JS = { ] + [ pth.replace(PROJECT_ROOT / 'static/', '') for pth in sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee'))\ - if pth not in courseware_only_js and pth not in discussion_js + if (pth not in courseware_only_js and + pth not in discussion_js and + pth not in staff_grading_js) ] + [ 'js/form.ext.js', 'js/my_courses_dropdown.js', @@ -505,7 +511,12 @@ PIPELINE_JS = { 'discussion' : { 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in discussion_js], 'output_filename': 'js/discussion.js' + }, + 'staff_grading' : { + 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in staff_grading_js], + 'output_filename': 'js/staff_grading.js' } + } PIPELINE_DISABLE_WRAPPER = True diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee new file mode 100644 index 0000000000..06c84a3867 --- /dev/null +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -0,0 +1,5 @@ +class @StaffGrading + constructor: -> + alert('hi!') + + \ No newline at end of file diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index b00fd935aa..d9d183c161 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -11,6 +11,7 @@ <%include file="/courseware/course_navigation.html" args="active_page='staff_grading'" /> <%block name="js_extra"> + <%static:js group='staff_grading'/>
From c7ab37bdabd0ea961837e20f158a3574f0d64bde Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 14 Nov 2012 14:32:43 -0500 Subject: [PATCH 079/736] initial js--called at page load --- .../coffee/src/staff_grading/staff_grading.coffee | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 06c84a3867..445b32b444 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -1,5 +1,11 @@ -class @StaffGrading +# wrap everything in a class in case we want to use inside xmodules later +class StaffGrading constructor: -> alert('hi!') - \ No newline at end of file + load: -> + alert('loading') + +# for now, just create an instance and load it... +grading = new StaffGrading +$(document).ready(grading.load) From aa786d138d8976057a005d4683139780c87aa42e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 14 Nov 2012 20:46:02 -0500 Subject: [PATCH 080/736] move computers--adding test file --- .../src/staff_grading/staff_grading.coffee | 11 ++++++--- .../src/staff_grading/test_grading.html | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 lms/static/coffee/src/staff_grading/test_grading.html diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 445b32b444..fbaf1cbfa8 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -1,11 +1,16 @@ # wrap everything in a class in case we want to use inside xmodules later class StaffGrading constructor: -> - alert('hi!') + @submission_container = $('.submission-container') + @rubric_container = $('.rubric-container') + @submit_button = $('.submit-button') + @mock_backend = true + @load() + + load: -> alert('loading') # for now, just create an instance and load it... -grading = new StaffGrading -$(document).ready(grading.load) +$(document).ready(() -> new StaffGrading) diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html new file mode 100644 index 0000000000..dd960b03ac --- /dev/null +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -0,0 +1,24 @@ + + + + + + + + +

Staff grading

+ +
+ +
+ +
+
+ +
+ +
+ + + + From 3d0ef7580710aeceb3b86aeaef2040c0bbc53986 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 14 Nov 2012 21:57:11 -0500 Subject: [PATCH 081/736] Basic js implementation, with mocked backend --- .../src/staff_grading/staff_grading.coffee | 121 ++++++++++++++++-- .../src/staff_grading/test_grading.html | 10 +- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index fbaf1cbfa8..4d754adfab 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -1,16 +1,121 @@ # wrap everything in a class in case we want to use inside xmodules later + +get_random_int: (min, max) -> + return Math.floor(Math.random() * (max - min + 1)) + min + +# states +state_grading = "have_data" +state_no_data = "no_data" +state_error = "error" + class StaffGrading - constructor: -> + constructor: (mock_backend) -> + @el = $('.staff-grading') + @ajax_url = @el.data('ajax_url') + + @error_container = $('.error-container') @submission_container = $('.submission-container') @rubric_container = $('.rubric-container') - @submit_button = $('.submit-button') - @mock_backend = true + @button = $('.submit-button') + @button.click @clicked + @state = state_no_data + + @mock_backend = mock_backend + if @mock_backend + @mock_cnt = 0 + + @get_next_submission() + + mock: (cmd, data) -> + # Return a mock response to cmd and data + @mock_cnt++ + if cmd == 'get_next' + response = + 'success': true + 'submission': 'submission! ' + @mock_cnt + 'rubric': 'A rubric!' + @mock_cnt + else if cmd == 'save_grade' + response = + 'success': true + 'submission': 'another submission! ' + @mock_cnt + 'rubric': 'A rubric!' + @mock_cnt + else + response = + 'success': false + 'error': 'Unknown command ' + cmd + return response + + + set_button_text: (text) -> + @button.prop('value', text) + + _post: (cmd, data, callback) -> + if @mock_backend + callback(@mock(cmd, data)) + else + # TODO: replace with postWithPrefix when that's loaded + $.post(@ajax_url + cmd, data, callback) + + + ajax_callback: (response) => + if response.success + if response.submission + @data_loaded(response.submission, response.rubric) + else + @no_more() + else + @error(response.error) + + get_next_submission: () -> + @_post('get_next', {}, @ajax_callback) + + submit_and_get_next: () -> + data = {eval: '123'} + @_post('save_grade', data, @ajax_callback) + + error: (msg) -> + @error_container.html(msg) + @state = state_error + @update() + + data_loaded: (submission, rubric) -> + @submission_container.html(submission) + @rubric_container.html(rubric) + @state = state_grading + @update() + + no_more: () -> + @submission_container.html(submission) + @rubric_container.html(rubric) + @state = state_no_data + @update() + + update: () -> + # make button state and actions right + if @state == state_error + @set_button_text('Try loading again') + else if @state == state_grading + @set_button_text('Submit') + else if @state == state_no_data + @set_button_text('Re-check for submissions') + else + @error('System got into invalid state ' + @state) + + clicked: (event) => + event.preventDefault() + if @state == state_error + @error_container.html('') + @get_next_submission() + else if @state == state_grading + @submit_and_get_next() + else if @state == state_no_data + @get_next_submission() + else + @error('System got into invalid state ' + @state) + - @load() - - load: -> - alert('loading') # for now, just create an instance and load it... -$(document).ready(() -> new StaffGrading) +mock_backend = true +$(document).ready(() -> new StaffGrading(mock_backend)) diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index dd960b03ac..07891e1c33 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -6,19 +6,19 @@ +

Staff grading

-
+
-
+
-
-
+
- +
From 546096e8a0a7fa22af779fd007b9a9829d380bac Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 15 Nov 2012 10:23:24 -0500 Subject: [PATCH 082/736] Split out mock backend, fix out-of-data bug --- .../src/staff_grading/staff_grading.coffee | 98 +++++++++++-------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 4d754adfab..22416a5276 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -8,10 +8,57 @@ state_grading = "have_data" state_no_data = "no_data" state_error = "error" +class StaffGradingBackend + constructor: (ajax_url, mock_backend) -> + @ajax_url = ajax_url + @mock_backend = mock_backend + if @mock_backend + @mock_cnt = 0 + + mock: (cmd, data) -> + # Return a mock response to cmd and data + @mock_cnt++ + if cmd == 'get_next' + response = + success: true + submission: 'submission! ' + @mock_cnt + rubric: 'A rubric!' + @mock_cnt + + else if cmd == 'save_grade' + response = + success: true + submission: 'another submission! ' + @mock_cnt + rubric: 'A rubric!' + @mock_cnt + else + response = + success: false + error: 'Unknown command ' + cmd + + if @mock_cnt % 5 == 0 + response = + success: true + message: 'No more submissions' + + + if @mock_cnt % 7 == 0 + response = + success: false + error: 'An error for testing' + + return response + + + post: (cmd, data, callback) -> + if @mock_backend + callback(@mock(cmd, data)) + else + # TODO: replace with postWithPrefix when that's loaded + $.post(@ajax_url + cmd, data, callback) + + class StaffGrading - constructor: (mock_backend) -> - @el = $('.staff-grading') - @ajax_url = @el.data('ajax_url') + constructor: (backend) -> + @backend = backend @error_container = $('.error-container') @submission_container = $('.submission-container') @@ -20,42 +67,12 @@ class StaffGrading @button.click @clicked @state = state_no_data - @mock_backend = mock_backend - if @mock_backend - @mock_cnt = 0 - @get_next_submission() - mock: (cmd, data) -> - # Return a mock response to cmd and data - @mock_cnt++ - if cmd == 'get_next' - response = - 'success': true - 'submission': 'submission! ' + @mock_cnt - 'rubric': 'A rubric!' + @mock_cnt - else if cmd == 'save_grade' - response = - 'success': true - 'submission': 'another submission! ' + @mock_cnt - 'rubric': 'A rubric!' + @mock_cnt - else - response = - 'success': false - 'error': 'Unknown command ' + cmd - return response - set_button_text: (text) -> @button.prop('value', text) - _post: (cmd, data, callback) -> - if @mock_backend - callback(@mock(cmd, data)) - else - # TODO: replace with postWithPrefix when that's loaded - $.post(@ajax_url + cmd, data, callback) - ajax_callback: (response) => if response.success @@ -67,11 +84,11 @@ class StaffGrading @error(response.error) get_next_submission: () -> - @_post('get_next', {}, @ajax_callback) + @backend.post('get_next', {}, @ajax_callback) submit_and_get_next: () -> data = {eval: '123'} - @_post('save_grade', data, @ajax_callback) + @backend.post('save_grade', data, @ajax_callback) error: (msg) -> @error_container.html(msg) @@ -85,8 +102,8 @@ class StaffGrading @update() no_more: () -> - @submission_container.html(submission) - @rubric_container.html(rubric) + @submission_container.html('') + @rubric_container.html('') @state = state_no_data @update() @@ -117,5 +134,8 @@ class StaffGrading # for now, just create an instance and load it... -mock_backend = true -$(document).ready(() -> new StaffGrading(mock_backend)) +mock_backend = true +ajax_url = $('.staff-grading').data('ajax_url') +backend = new StaffGradingBackend(ajax_url, mock_backend) + +$(document).ready(() -> new StaffGrading(backend)) From b1d5273a2a10b828a137714d2e03a4e0fe4f9aea Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 15 Nov 2012 10:45:55 -0500 Subject: [PATCH 083/736] add messages, headers for sections --- .../src/staff_grading/staff_grading.coffee | 25 ++++++++++++++----- .../src/staff_grading/test_grading.html | 22 ++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 22416a5276..e36e70151f 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -61,12 +61,18 @@ class StaffGrading @backend = backend @error_container = $('.error-container') + @message_container = $('.message-container') @submission_container = $('.submission-container') @rubric_container = $('.rubric-container') + @submission_wrapper = $('.submission-wrapper') + @rubric_wrapper = $('.rubric-wrapper') @button = $('.submit-button') @button.click @clicked @state = state_no_data + @submission_wrapper.hide() + @rubric_wrapper.hide() + @get_next_submission() @@ -102,26 +108,35 @@ class StaffGrading @update() no_more: () -> - @submission_container.html('') - @rubric_container.html('') @state = state_no_data @update() update: () -> - # make button state and actions right + # make button and div state match the state. Idempotent. if @state == state_error @set_button_text('Try loading again') + else if @state == state_grading + @submission_wrapper.show() + @rubric_wrapper.show() @set_button_text('Submit') + else if @state == state_no_data + @submission_wrapper.hide() + @rubric_wrapper.hide() + @message_container.html('Nothing to grade') @set_button_text('Re-check for submissions') + else @error('System got into invalid state ' + @state) clicked: (event) => event.preventDefault() + # always clear out errors and messages on transition... + @message_container.html('') + @error_container.html('') + if @state == state_error - @error_container.html('') @get_next_submission() else if @state == state_grading @submit_and_get_next() @@ -131,8 +146,6 @@ class StaffGrading @error('System got into invalid state ' + @state) - - # for now, just create an instance and load it... mock_backend = true ajax_url = $('.staff-grading').data('ajax_url') diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index 07891e1c33..780515a752 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -1,7 +1,7 @@ - + @@ -9,11 +9,23 @@

Staff grading

-
- -
+
+
-
+
+
+ +
+

Submission

+
+
+
+ +
+

Rubric

+
+
+
From e1fd6d73b3ebd80fb695d0d2d775506fdacf96e3 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 15 Nov 2012 13:35:53 -0500 Subject: [PATCH 084/736] refactor to be more clearly model-view --- .../src/staff_grading/staff_grading.coffee | 83 ++++++++++++------- .../src/staff_grading/test_grading.html | 1 + 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index e36e70151f..827f9fe56e 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -25,6 +25,7 @@ class StaffGradingBackend rubric: 'A rubric!' + @mock_cnt else if cmd == 'save_grade' + console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") response = success: true submission: 'another submission! ' + @mock_cnt @@ -60,6 +61,7 @@ class StaffGrading constructor: (backend) -> @backend = backend + # all the jquery selectors @error_container = $('.error-container') @message_container = $('.message-container') @submission_container = $('.submission-container') @@ -67,75 +69,98 @@ class StaffGrading @submission_wrapper = $('.submission-wrapper') @rubric_wrapper = $('.rubric-wrapper') @button = $('.submit-button') - @button.click @clicked + + # model state @state = state_no_data + @submission = '' + @rubric = '' + @error_msg = '' + @message = '' - @submission_wrapper.hide() - @rubric_wrapper.hide() + @feedback = null + @score = null + # action handlers + @button.click @clicked + + # render intial state + @render_view() + + # send initial request automatically @get_next_submission() set_button_text: (text) -> @button.prop('value', text) - ajax_callback: (response) => - if response.success - if response.submission - @data_loaded(response.submission, response.rubric) - else - @no_more() + # always clear out errors and messages on transition. + @error_msg = '' + @message = '' + + if response.success + if response.submission + @data_loaded(response.submission, response.rubric) else - @error(response.error) - + @no_more() + else + @error(response.error) + + @render_view() + get_next_submission: () -> @backend.post('get_next', {}, @ajax_callback) submit_and_get_next: () -> - data = {eval: '123'} + data = {score: '1', feedback: 'Great!'} + @backend.post('save_grade', data, @ajax_callback) error: (msg) -> - @error_container.html(msg) + @error_msg = msg @state = state_error - @update() data_loaded: (submission, rubric) -> - @submission_container.html(submission) - @rubric_container.html(rubric) + @submission = submission + @rubric = rubric @state = state_grading - @update() no_more: () -> + @submission = null + @rubric = null + @message = 'Nothing to grade' @state = state_no_data - @update() - update: () -> - # make button and div state match the state. Idempotent. + render_view: () -> + # make the view elements match the state. Idempotent. + show_grading_elements = false + + @message_container.html(@message) + @error_container.html(@error_msg) + if @state == state_error @set_button_text('Try loading again') else if @state == state_grading - @submission_wrapper.show() - @rubric_wrapper.show() + @submission_container.html(@submission) + @rubric_container.html(@rubric) + show_grading_elements = true @set_button_text('Submit') else if @state == state_no_data - @submission_wrapper.hide() - @rubric_wrapper.hide() - @message_container.html('Nothing to grade') + @message_container.html(@message) @set_button_text('Re-check for submissions') else @error('System got into invalid state ' + @state) + @submission_wrapper.toggle(show_grading_elements) + @rubric_wrapper.toggle(show_grading_elements) + + clicked: (event) => event.preventDefault() - # always clear out errors and messages on transition... - @message_container.html('') - @error_container.html('') - + if @state == state_error @get_next_submission() else if @state == state_grading diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index 780515a752..8e69c90866 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -25,6 +25,7 @@

Rubric

+
From e910f60e08b2418fa118680ff785e5795f66e036 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 15 Nov 2012 14:27:46 -0500 Subject: [PATCH 085/736] wip --- .../src/staff_grading/staff_grading.coffee | 18 +++++++++++------- .../coffee/src/staff_grading/test_grading.html | 12 ++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 827f9fe56e..aded7464e7 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -22,7 +22,7 @@ class StaffGradingBackend response = success: true submission: 'submission! ' + @mock_cnt - rubric: 'A rubric!' + @mock_cnt + rubric: 'A rubric! ' + @mock_cnt else if cmd == 'save_grade' console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") @@ -68,7 +68,8 @@ class StaffGrading @rubric_container = $('.rubric-container') @submission_wrapper = $('.submission-wrapper') @rubric_wrapper = $('.rubric-wrapper') - @button = $('.submit-button') + @feedback_area = $('.feedback-area') + @submit_button = $('.submit-button') # model state @state = state_no_data @@ -77,11 +78,12 @@ class StaffGrading @error_msg = '' @message = '' - @feedback = null @score = null # action handlers - @button.click @clicked + @submit_button.click @submit + @correct_button.click () => @score = 1 + @incorrect_button.click () => @score = 0 # render intial state @render_view() @@ -91,7 +93,7 @@ class StaffGrading set_button_text: (text) -> - @button.prop('value', text) + @submit_button.prop('value', text) ajax_callback: (response) => # always clear out errors and messages on transition. @@ -112,7 +114,7 @@ class StaffGrading @backend.post('get_next', {}, @ajax_callback) submit_and_get_next: () -> - data = {score: '1', feedback: 'Great!'} + data = {score: @score, feedback: @feedback_area.val()} @backend.post('save_grade', data, @ajax_callback) @@ -123,6 +125,8 @@ class StaffGrading data_loaded: (submission, rubric) -> @submission = submission @rubric = rubric + @feedback_area.val('') + @score = null @state = state_grading no_more: () -> @@ -158,7 +162,7 @@ class StaffGrading @rubric_wrapper.toggle(show_grading_elements) - clicked: (event) => + submit: (event) => event.preventDefault() if @state == state_error diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index 8e69c90866..b417aa41d9 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -26,6 +26,18 @@
+
+ +

+ + + + + +

+
+
From 5836438454225cc8c2297d43e490001f7784ce57 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 15 Nov 2012 19:23:07 -0500 Subject: [PATCH 086/736] hook up radio buttons, hide submit button till after grading --- .../src/staff_grading/staff_grading.coffee | 34 +++++++++++++++---- .../src/staff_grading/test_grading.html | 6 ++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index aded7464e7..b252e7a365 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -4,7 +4,8 @@ get_random_int: (min, max) -> return Math.floor(Math.random() * (max - min + 1)) + min # states -state_grading = "have_data" +state_grading = "grading" +state_graded = "graded" state_no_data = "no_data" state_error = "error" @@ -82,8 +83,10 @@ class StaffGrading # action handlers @submit_button.click @submit - @correct_button.click () => @score = 1 - @incorrect_button.click () => @score = 0 + # TODO: hook up an event to the input changing, which updates + # @score (instead of the individual hacks) + $('#correct-radio').click @graded_callback + $('#incorrect-radio').click @graded_callback # render intial state @render_view() @@ -92,8 +95,13 @@ class StaffGrading @get_next_submission() - set_button_text: (text) -> - @submit_button.prop('value', text) + set_button_text: (text) => + @submit_button.attr('value', text) + + graded_callback: (event) => + @score = event.target.value + @state = state_graded + @render_view() ajax_callback: (response) => # always clear out errors and messages on transition. @@ -138,6 +146,7 @@ class StaffGrading render_view: () -> # make the view elements match the state. Idempotent. show_grading_elements = false + show_submit_button = true @message_container.html(@message) @error_container.html(@error_msg) @@ -149,6 +158,16 @@ class StaffGrading @submission_container.html(@submission) @rubric_container.html(@rubric) show_grading_elements = true + + # no submit button until user picks grade. + show_submit_button = false + + # TODO: clean up with proper input-related logic + $('#correct-radio')[0].checked = false + $('#incorrect-radio')[0].checked = false + + else if @state == state_graded + show_grading_elements = true @set_button_text('Submit') else if @state == state_no_data @@ -158,6 +177,7 @@ class StaffGrading else @error('System got into invalid state ' + @state) + @submit_button.toggle(show_submit_button) @submission_wrapper.toggle(show_grading_elements) @rubric_wrapper.toggle(show_grading_elements) @@ -167,12 +187,12 @@ class StaffGrading if @state == state_error @get_next_submission() - else if @state == state_grading + else if @state == state_graded @submit_and_get_next() else if @state == state_no_data @get_next_submission() else - @error('System got into invalid state ' + @state) + @error('System got into invalid state for submission: ' + @state) # for now, just create an instance and load it... diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index b417aa41d9..8a1cde1fd4 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -1,7 +1,8 @@ - + + @@ -32,8 +33,7 @@

- - +

From 9d8d6655c685597ada8885e28a889a92d4fe288e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 09:10:02 -0500 Subject: [PATCH 087/736] add docstring for expect_json decorator --- common/djangoapps/util/json_request.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/djangoapps/util/json_request.py b/common/djangoapps/util/json_request.py index 169a7e3fb4..9458bff858 100644 --- a/common/djangoapps/util/json_request.py +++ b/common/djangoapps/util/json_request.py @@ -4,6 +4,11 @@ import json def expect_json(view_function): + """ + View decorator for simplifying handing of requests that expect json. If the request's + CONTENT_TYPE is application/json, parses the json dict from request.body, and updates + request.POST with the contents. + """ @wraps(view_function) def expect_json_with_cloned_request(request, *args, **kwargs): # cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information From ed6a8f68ac7c422dfd198ea1a582d186d4a3da53 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 10:04:56 -0500 Subject: [PATCH 088/736] starting to stub out the backend staff grading service [wip] --- .../instructor/staff_grading_service.py | 57 +++++++++++++++++++ lms/urls.py | 4 ++ 2 files changed, 61 insertions(+) create mode 100644 lms/djangoapps/instructor/staff_grading_service.py diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py new file mode 100644 index 0000000000..d4a4d82e63 --- /dev/null +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -0,0 +1,57 @@ +""" +This module provides views that proxy to the staff grading backend service. +""" + +import json +import requests +import sys + +from django.http import Http404 +from django.http import HttpResponse + + +from util.json_request import expect_json + +class GradingServiceError(Exception): + pass + +class StaffGradingService(object): + """ + Interface to staff grading backend. + """ + def __init__(self, url): + self.url = url + # TODO: add auth + self.session = requests.session() + + def get_next(course_id): + """ + Get the next thing to grade. Returns json, or raises GradingServiceError + if there's a problem. + """ + try: + r = self.session.get(url + 'get_next') + except requests.exceptions.ConnectionError as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + + +#@login_required +def get_next(request, course_id): + """ + """ + d = {'success': False} + return HttpResponse(json.dumps(d)) + + +#@login_required +@expect_json +def save_grade(request, course_id): + """ + + """ + d = {'success': False} + return HttpResponse(json.dumps(d)) + diff --git a/lms/urls.py b/lms/urls.py index 7c1ccb6bde..0b2def9135 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -232,6 +232,10 @@ if settings.COURSEWARE_ENABLED: 'instructor.views.enroll_students', name='enroll_students'), url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading$', 'instructor.views.staff_grading', name='staff_grading'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/get_next$', + 'instructor.staff_grading_service.get_next', name='staff_grading_get_next'), + url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$', + 'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'), ) # discussion forums live within courseware, so courseware must be enabled first From 4f359ea59412c7f1aba2b25912075dbcc8f61779 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 13:38:21 -0500 Subject: [PATCH 089/736] pass through message when no more submissions. --- lms/static/coffee/src/staff_grading/staff_grading.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index b252e7a365..e410a3c41d 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -112,7 +112,7 @@ class StaffGrading if response.submission @data_loaded(response.submission, response.rubric) else - @no_more() + @no_more(response.message) else @error(response.error) @@ -137,10 +137,10 @@ class StaffGrading @score = null @state = state_grading - no_more: () -> + no_more: (message) -> @submission = null @rubric = null - @message = 'Nothing to grade' + @message = message @state = state_no_data render_view: () -> From 5bf39fef962c1e94e1f616074d31cc57a710e784 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 15:23:05 -0500 Subject: [PATCH 090/736] Cherry pick the test-cleanup parts of e2826cb. - look up test courses by id, not name --- .../xmodule/xmodule/modulestore/__init__.py | 13 ++++++++++ lms/djangoapps/courseware/tests/tests.py | 19 ++++---------- lms/djangoapps/instructor/tests.py | 25 ++++++++++--------- lms/djangoapps/instructor/views.py | 4 +-- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index fa8cf8d3d7..5b94add68f 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -339,6 +339,12 @@ class ModuleStore(object): ''' raise NotImplementedError + def get_course(self, course_id): + ''' + Look for a specific course id. Returns the course descriptor, or None if not found. + ''' + raise NotImplementedError + def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed for path_to_location(). @@ -399,3 +405,10 @@ class ModuleStoreBase(ModuleStore): errorlog = self._get_errorlog(location) return errorlog.errors + + def get_course(self, course_id): + """Default impl--linear search through course list""" + for c in self.get_courses(): + if c.id == course_id: + return c + return None diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 8239eadfd9..1eebf0f408 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -288,14 +288,10 @@ class TestNavigation(PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(course_id): - """Assumes the course is present""" - return [c for c in courses if c.id==course_id][0] - - self.full = find_course("edX/full/6.002_Spring_2012") - self.toy = find_course("edX/toy/2012_Fall") + # Assume courses are there + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -346,14 +342,9 @@ class TestViewAuth(PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() - def find_course(course_id): - """Assumes the course is present""" - return [c for c in courses if c.id==course_id][0] - - self.full = find_course("edX/full/6.002_Spring_2012") - self.toy = find_course("edX/toy/2012_Fall") + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 532c0c3f68..55e63c8dc2 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -33,12 +33,8 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() - def find_course(name): - """Assumes the course is present""" - return [c for c in courses if c.location.course==name][0] - - self.full = find_course("full") - self.toy = find_course("toy") + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -49,9 +45,12 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): self.activate_user(self.student) self.activate_user(self.instructor) - group_name = _course_staff_group_name(self.toy.location) - g = Group.objects.create(name=group_name) - g.user_set.add(ct.user(self.instructor)) + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(ct.user(self.instructor)) + + make_instructor(self.toy) self.logout() self.login(self.instructor, self.password) @@ -67,9 +66,9 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): self.assertEqual(response['Content-Type'],'text/csv',msg) - cdisp = response['Content-Disposition'].replace('TT_2012','2012') # jenkins course_id is TT_2012_Fall instead of 2012_Fall? - msg += "cdisp = '{0}'\n".format(cdisp) - self.assertEqual(cdisp,'attachment; filename=grades_edX/toy/2012_Fall.csv',msg) + cdisp = response['Content-Disposition'] + msg += "Content-Disposition = '%s'\n" % cdisp + self.assertEqual(cdisp, 'attachment; filename=grades_{0}.csv'.format(course.id), msg) body = response.content.replace('\r','') msg += "body = '{0}'\n".format(body) @@ -77,6 +76,8 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0" ''' + # All the not-actually-in-the-course hw and labs come from the + # default grading policy string in graders.py self.assertEqual(body, expected_body, msg) FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ] diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 8a33f1d60b..b877a7236d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -90,7 +90,7 @@ def instructor_dashboard(request, course_id): try: group = Group.objects.get(name=staffgrp) except Group.DoesNotExist: - group = Group(name=staffgrp) # create the group + group = Group(name=staffgrp) # create the group group.save() return group @@ -380,7 +380,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] - if get_grades: + if get_grades and enrolled_students.count() > 0: # just to construct the header gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) From 4b8708c2e4278bae5f005f212c79983f0a8f5a88 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 15:24:11 -0500 Subject: [PATCH 091/736] move a comment --- lms/djangoapps/instructor/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 55e63c8dc2..4f8ac140b0 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -73,11 +73,11 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): body = response.content.replace('\r','') msg += "body = '{0}'\n".format(body) + # All the not-actually-in-the-course hw and labs come from the + # default grading policy string in graders.py expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final" "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0" ''' - # All the not-actually-in-the-course hw and labs come from the - # default grading policy string in graders.py self.assertEqual(body, expected_body, msg) FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ] From 055aeae0b6b75bf31c733a0f618a24f319f92c74 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 15:47:49 -0500 Subject: [PATCH 092/736] fix comment in access.py --- lms/djangoapps/courseware/access.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 00b4c763b3..ba9b8a3bc0 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -34,7 +34,8 @@ def has_access(user, obj, action): user: a Django user object. May be anonymous. - obj: The object to check access for. For now, a module or descriptor. + obj: The object to check access for. A module, descriptor, location, or + certain special strings (e.g. 'global') action: A string specifying the action that the client is trying to perform. From ff1192657015e67f0f057309842fa094d867a2e7 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 15:48:38 -0500 Subject: [PATCH 093/736] Initial impl and basic access tests for staff grading service --- lms/djangoapps/courseware/tests/tests.py | 14 +- .../instructor/staff_grading_service.py | 120 +++++++++++++++++- lms/djangoapps/instructor/tests.py | 54 ++++++-- lms/envs/common.py | 4 + 4 files changed, 175 insertions(+), 17 deletions(-) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 1eebf0f408..eb026e9c6b 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -221,8 +221,7 @@ class PageLoader(ActivateLoginTestCase): def check_for_get_code(self, code, url): """ - Check that we got the expected code. Hacks around our broken 404 - handling. + Check that we got the expected code when accessing url via GET. """ resp = self.client.get(url) self.assertEqual(resp.status_code, code, @@ -230,6 +229,17 @@ class PageLoader(ActivateLoginTestCase): .format(resp.status_code, url, code)) + def check_for_post_code(self, code, url, data={}): + """ + Check that we got the expected code when accessing url via POST. + """ + resp = self.client.post(url, data) + self.assertEqual(resp.status_code, code, + "got code {0} for url '{1}'. Expected code {2}" + .format(resp.status_code, url, code)) + + + def check_pages_load(self, course_name, data_dir, modstore): """Make all locations in course load""" print "Checking course {0} in {1}".format(course_name, data_dir) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index d4a4d82e63..e4f82cd5e0 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -3,14 +3,20 @@ This module provides views that proxy to the staff grading backend service. """ import json +import logging import requests import sys +from django.conf import settings from django.http import Http404 from django.http import HttpResponse - +from courseware.access import has_access from util.json_request import expect_json +from xmodule.course_module import CourseDescriptor + +log = logging.getLogger("mitx.courseware") + class GradingServiceError(Exception): pass @@ -37,21 +43,121 @@ class StaffGradingService(object): return r.text + def save_grade(course_id, submission_id, score, feedback): + """ + Save a grade. + + TODO: what is data? + + Returns json, or raises GradingServiceError if there's a problem. + """ + try: + r = self.session.get(url + 'save_grade') + except requests.exceptions.ConnectionError as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + +_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) + + +def _err_response(msg): + """ + Return a HttpResponse with a json dump with success=False, and the given error message. + """ + return HttpResponse(json.dumps({'success': False, 'error': msg})) + + +def _check_access(user, course_id): + """ + Raise 404 if user doesn't have staff access to course_id + """ + course_location = CourseDescriptor.id_to_location(course_id) + if not has_access(user, course_location, 'staff'): + raise Http404 + + return + -#@login_required def get_next(request, course_id): """ + Get the next thing to grade for course_id. + + Returns a json dict with the following keys: + + 'success': bool + + 'submission_id': a unique identifier for the submission, to be passed back + with the grade. + + 'submission': the submission, rendered as read-only html for grading + + 'rubric': the rubric, also rendered as html. + + 'message': if there was no submission available, but nothing went wrong, + there will be a message field. + + 'error': if success is False, will have an error message with more info. """ - d = {'success': False} - return HttpResponse(json.dumps(d)) + _check_access(request.user, course_id) + + return HttpResponse(_get_next(course_id)) + + +def _get_next(course_id): + """ + Implementation of get_next (also called from save_grade) -- returns a json string + """ + + try: + return _service.get_next(course_id) + except GradingServiceError: + log.exception("Error from grading service") + return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) -#@login_required @expect_json def save_grade(request, course_id): """ + Save the grade and feedback for a submission, and, if all goes well, return + the next thing to grade. + Expects the following POST parameters: + 'score': int + 'feedback': string + 'submission_id': int + + Returns the same thing as get_next, except that additional error messages + are possible if something goes wrong with saving the grade. """ - d = {'success': False} - return HttpResponse(json.dumps(d)) + _check_access(request.user, course_id) + + if request.method != 'POST': + raise Http404 + + required = ('score', 'feedback', 'submission_id') + for k in required: + if k not in request.POST.keys(): + return _err_response('Missing required key {0}'.format(k)) + + p = request.POST + + try: + result_json = _service.save_grade(course_id, p['submission_id'], p['score'], p['feedback']) + except GradingServiceError: + log.exception("Error saving grade") + return _err_response('Could not connect to grading service') + + try: + result = json.loads(result_json) + except ValueError: + return _err_response('Grading service returned mal-formatted data.') + + if not result.get('success', False): + log.warning('Got success=False from grading service. Response: %s', result_json) + return _err_response('Grading service failed') + + # Ok, save_grade seemed to work. Get the next submission to grade. + return HttpResponse(_get_next(course_id)) diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 4f8ac140b0..5f740b8ff9 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -31,7 +31,6 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} - courses = modulestore().get_courses() self.full = modulestore().get_course("edX/full/6.002_Spring_2012") self.toy = modulestore().get_course("edX/toy/2012_Fall") @@ -79,6 +78,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): "2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0" ''' self.assertEqual(body, expected_body, msg) + FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ] FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'} @@ -90,22 +90,19 @@ def action_name(operation, rolename): else: return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) + @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) class TestInstructorDashboardForumAdmin(ct.PageLoader): ''' Check for change in forum admin role memberships ''' - + def setUp(self): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() - def find_course(name): - """Assumes the course is present""" - return [c for c in courses if c.location.course==name][0] - - self.full = find_course("full") - self.toy = find_course("toy") + self.full = modulestore().get_course("edX/full/6.002_Spring_2012") + self.toy = modulestore().get_course("edX/toy/2012_Fall") # Create two accounts self.student = 'view@test.com' @@ -124,6 +121,8 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): self.login(self.instructor, self.password) self.enroll(self.toy) + + def initialize_roles(self, course_id): self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0] self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0] @@ -210,3 +209,42 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): added_roles.sort() roles = ', '.join(added_roles) self.assertTrue(response.content.find('{0}'.format(roles))>=0, 'not finding roles "{0}"'.format(roles)) + + +@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) +class TestStaffGradingService(ct.PageLoader): + ''' + Check that staff grading service proxy works. Basically just checking the + access control and error handling logic -- all the actual work is on the + backend. + ''' + + + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.course_id = "edX/toy/2012_Fall" + self.toy = modulestore().get_course(self.course_id) + def make_instructor(course): + group_name = _course_staff_group_name(course.location) + g = Group.objects.create(name=group_name) + g.user_set.add(ct.user(self.instructor)) + + make_instructor(self.toy) + + self.logout() + + def test_access(self): + """ + Make sure only staff have access. + """ + self.login(self.student, self.password) + self.enroll(self.toy) + + # both get and post should return 404 + for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): + url = reverse(view_name, kwargs={'course_id': self.course_id}) + self.check_for_get_code(404, url) + self.check_for_post_code(404, url) + diff --git a/lms/envs/common.py b/lms/envs/common.py index 008de7ac84..7ddd2ffb9a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -322,6 +322,10 @@ WIKI_USE_BOOTSTRAP_SELECT_WIDGET = False WIKI_LINK_LIVE_LOOKUPS = False WIKI_LINK_DEFAULT_LEVEL = 2 +################################# Staff grading config ##################### + +STAFF_GRADING_BACKEND_URL = None + ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' From d0e2b85e3c0b876263d3e91da41364219f9a6a0b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 16:56:12 -0500 Subject: [PATCH 094/736] Refactor testing code, hook up frontend. - now getting requests from js to server and back, with mocked service. --- lms/djangoapps/courseware/tests/tests.py | 102 +++++++++--------- .../instructor/staff_grading_service.py | 31 ++++-- lms/djangoapps/instructor/tests.py | 58 ++++++++-- lms/djangoapps/instructor/views.py | 5 + .../src/staff_grading/staff_grading.coffee | 16 ++- lms/templates/instructor/staff_grading.html | 43 +++++++- 6 files changed, 187 insertions(+), 68 deletions(-) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index eb026e9c6b..480a119b48 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -222,24 +222,28 @@ class PageLoader(ActivateLoginTestCase): def check_for_get_code(self, code, url): """ Check that we got the expected code when accessing url via GET. + Returns the response. """ resp = self.client.get(url) self.assertEqual(resp.status_code, code, "got code {0} for url '{1}'. Expected code {2}" .format(resp.status_code, url, code)) + return resp def check_for_post_code(self, code, url, data={}): """ Check that we got the expected code when accessing url via POST. + Returns the response. """ resp = self.client.post(url, data) self.assertEqual(resp.status_code, code, "got code {0} for url '{1}'. Expected code {2}" .format(resp.status_code, url, code)) + return resp + - def check_pages_load(self, course_name, data_dir, modstore): """Make all locations in course load""" print "Checking course {0} in {1}".format(course_name, data_dir) @@ -661,46 +665,46 @@ class TestCourseGrader(PageLoader): return [c for c in courses if c.id==course_id][0] self.graded_course = find_course("edX/graded/2012_Fall") - + # create a test student self.student = 'view@test.com' self.password = 'foo' self.create_account('u1', self.student, self.password) self.activate_user(self.student) self.enroll(self.graded_course) - + self.student_user = user(self.student) - + self.factory = RequestFactory() - + def get_grade_summary(self): student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( self.graded_course.id, self.student_user, self.graded_course) - - fake_request = self.factory.get(reverse('progress', - kwargs={'course_id': self.graded_course.id})) - - return grades.grade(self.student_user, fake_request, - self.graded_course, student_module_cache) - - def get_homework_scores(self): - return self.get_grade_summary()['totaled_scores']['Homework'] - - def get_progress_summary(self): - student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( - self.graded_course.id, self.student_user, self.graded_course) - + fake_request = self.factory.get(reverse('progress', kwargs={'course_id': self.graded_course.id})) - progress_summary = grades.progress_summary(self.student_user, fake_request, + return grades.grade(self.student_user, fake_request, + self.graded_course, student_module_cache) + + def get_homework_scores(self): + return self.get_grade_summary()['totaled_scores']['Homework'] + + def get_progress_summary(self): + student_module_cache = StudentModuleCache.cache_for_descriptor_descendents( + self.graded_course.id, self.student_user, self.graded_course) + + fake_request = self.factory.get(reverse('progress', + kwargs={'course_id': self.graded_course.id})) + + progress_summary = grades.progress_summary(self.student_user, fake_request, self.graded_course, student_module_cache) return progress_summary - + def check_grade_percent(self, percent): grade_summary = self.get_grade_summary() - self.assertEqual(percent, grade_summary['percent']) - + self.assertEqual(grade_summary['percent'], percent) + def submit_question_answer(self, problem_url_name, responses): """ The field names of a problem are hard to determine. This method only works @@ -710,96 +714,96 @@ class TestCourseGrader(PageLoader): input_i4x-edX-graded-problem-H1P3_2_2 """ problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name) - - modx_url = reverse('modx_dispatch', + + modx_url = reverse('modx_dispatch', kwargs={ 'course_id' : self.graded_course.id, 'location' : problem_location, 'dispatch' : 'problem_check', } ) - + resp = self.client.post(modx_url, { 'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0], 'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1], }) print "modx_url" , modx_url, "responses" , responses print "resp" , resp - + return resp - + def problem_location(self, problem_url_name): return "i4x://edX/graded/problem/{0}".format(problem_url_name) - + def reset_question_answer(self, problem_url_name): problem_location = self.problem_location(problem_url_name) - - modx_url = reverse('modx_dispatch', + + modx_url = reverse('modx_dispatch', kwargs={ 'course_id' : self.graded_course.id, 'location' : problem_location, 'dispatch' : 'problem_reset', } ) - + resp = self.client.post(modx_url) - return resp - + return resp + def test_get_graded(self): #### Check that the grader shows we have 0% in the course self.check_grade_percent(0) - + #### Submit the answers to a few problems as ajax calls def earned_hw_scores(): """Global scores, each Score is a Problem Set""" return [s.earned for s in self.get_homework_scores()] - + def score_for_hw(hw_url_name): hw_section = [section for section in self.get_progress_summary()[0]['sections'] if section.get('url_name') == hw_url_name][0] return [s.earned for s in hw_section['scores']] - + # Only get half of the first problem correct self.submit_question_answer('H1P1', ['Correct', 'Incorrect']) self.check_grade_percent(0.06) self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0]) - + # Get both parts of the first problem correct self.reset_question_answer('H1P1') self.submit_question_answer('H1P1', ['Correct', 'Correct']) self.check_grade_percent(0.13) self.assertEqual(earned_hw_scores(), [2.0, 0, 0]) self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0]) - + # This problem is shown in an ABTest self.submit_question_answer('H1P2', ['Correct', 'Correct']) self.check_grade_percent(0.25) self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0]) - self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - + self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) + # This problem is hidden in an ABTest. Getting it correct doesn't change total grade self.submit_question_answer('H1P3', ['Correct', 'Correct']) self.check_grade_percent(0.25) self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) - + # On the second homework, we only answer half of the questions. # Then it will be dropped when homework three becomes the higher percent # This problem is also weighted to be 4 points (instead of default of 2) - # If the problem was unweighted the percent would have been 0.38 so we + # If the problem was unweighted the percent would have been 0.38 so we # know it works. self.submit_question_answer('H2P1', ['Correct', 'Correct']) self.check_grade_percent(0.42) - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0]) + # Third homework self.submit_question_answer('H3P1', ['Correct', 'Correct']) self.check_grade_percent(0.42) # Score didn't change - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0]) + self.submit_question_answer('H3P2', ['Correct', 'Correct']) self.check_grade_percent(0.5) # Now homework2 dropped. Score changes - self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) - + self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0]) + # Now we answer the final question (worth half of the grade) self.submit_question_answer('FinalQuestion', ['Correct', 'Correct']) self.check_grade_percent(1.0) # Hooray! We got 100% diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index e4f82cd5e0..2a2a7a3552 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -21,6 +21,25 @@ log = logging.getLogger("mitx.courseware") class GradingServiceError(Exception): pass + +class MockStaffGradingService(object): + """ + A simple mockup of a staff grading service, testing. + """ + def __init__(self): + self.cnt = 0 + + def get_next(self, course_id): + self.cnt += 1 + return json.dumps({'success': True, + 'submission_id': self.cnt, + 'submission': 'Test submission {cnt}'.format(cnt=self.cnt), + 'rubric': 'A rubric'}) + + def save_grade(self, course_id, submission_id, score, feedback): + return self.get_next(course_id) + + class StaffGradingService(object): """ Interface to staff grading backend. @@ -30,20 +49,20 @@ class StaffGradingService(object): # TODO: add auth self.session = requests.session() - def get_next(course_id): + def get_next(self, course_id): """ Get the next thing to grade. Returns json, or raises GradingServiceError if there's a problem. """ try: - r = self.session.get(url + 'get_next') + r = self.session.get(self.url + 'get_next') except requests.exceptions.ConnectionError as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] return r.text - def save_grade(course_id, submission_id, score, feedback): + def save_grade(self, course_id, submission_id, score, feedback): """ Save a grade. @@ -52,15 +71,15 @@ class StaffGradingService(object): Returns json, or raises GradingServiceError if there's a problem. """ try: - r = self.session.get(url + 'save_grade') + r = self.session.get(self.url + 'save_grade') except requests.exceptions.ConnectionError as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] return r.text -_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) - +#_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) +_service = MockStaffGradingService() def _err_response(msg): """ diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 5f740b8ff9..28a56ad9de 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -8,15 +8,24 @@ Notes for running by hand: django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor """ +import courseware.tests.tests as ct + +import json + +from nose import SkipTest +from mock import patch, Mock + from override_settings import override_settings -from django.contrib.auth.models import \ - Group # Need access to internal func to put users in the right group +# Need access to internal func to put users in the right group +from django.contrib.auth.models import Group + from django.core.urlresolvers import reverse from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \ FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT from django_comment_client.utils import has_forum_access +from instructor import staff_grading_service from courseware.access import _course_staff_group_name import courseware.tests.tests as ct from xmodule.modulestore.django import modulestore @@ -79,7 +88,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader): ''' self.assertEqual(body, expected_body, msg) - + FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ] FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'} FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'} @@ -91,6 +100,8 @@ def action_name(operation, rolename): return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename]) +_mock_service = staff_grading_service.MockStaffGradingService() + @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) class TestInstructorDashboardForumAdmin(ct.PageLoader): ''' @@ -101,8 +112,15 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() +<<<<<<< HEAD self.full = modulestore().get_course("edX/full/6.002_Spring_2012") self.toy = modulestore().get_course("edX/toy/2012_Fall") +======= + + + self.course_id = "edX/toy/2012_Fall" + self.toy = modulestore().get_course(self.course_id) +>>>>>>> Refactor testing code, hook up frontend. # Create two accounts self.student = 'view@test.com' @@ -122,7 +140,7 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): self.enroll(self.toy) - + def initialize_roles(self, course_id): self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0] self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0] @@ -220,7 +238,7 @@ class TestStaffGradingService(ct.PageLoader): ''' - + def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -240,7 +258,6 @@ class TestStaffGradingService(ct.PageLoader): Make sure only staff have access. """ self.login(self.student, self.password) - self.enroll(self.toy) # both get and post should return 404 for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): @@ -248,3 +265,32 @@ class TestStaffGradingService(ct.PageLoader): self.check_for_get_code(404, url) self.check_for_post_code(404, url) +<<<<<<< HEAD +======= + + @patch.object(staff_grading_service, '_service', _mock_service) + def test_get_next(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) + + r = self.check_for_get_code(200, url) + d = json.loads(r.content) + self.assertTrue(d['success']) + self.assertEquals(d['submission_id'], _mock_service.cnt) + + + @patch.object(staff_grading_service, '_service', _mock_service) + def test_save_grade(self): + self.login(self.instructor, self.password) + + url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) + + data = {'score': '12', 'feedback': 'great!', 'submission_id': '123'} + r = self.check_for_post_code(200, url, data) + d = json.loads(r.content) + self.assertTrue(d['success'], str(d)) + self.assertEquals(d['submission_id'], _mock_service.cnt) + + +>>>>>>> Refactor testing code, hook up frontend. diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b877a7236d..389a64721a 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -422,10 +422,15 @@ def staff_grading(request, course_id): grading = StaffGrading(course) + ajax_url = reverse('staff_grading', kwargs={'course_id': course_id}) + if not ajax_url.endswith('/'): + ajax_url += '/' + return render_to_response('instructor/staff_grading.html', { 'view_html': grading.get_html(), 'course': course, 'course_id': course_id, + 'ajax_url': ajax_url, # Checked above 'staff_access': True, }) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index e410a3c41d..7440277d98 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -24,6 +24,7 @@ class StaffGradingBackend success: true submission: 'submission! ' + @mock_cnt rubric: 'A rubric! ' + @mock_cnt + submission_id: @mock_cnt else if cmd == 'save_grade' console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") @@ -31,6 +32,7 @@ class StaffGradingBackend success: true submission: 'another submission! ' + @mock_cnt rubric: 'A rubric!' + @mock_cnt + submission_id: @mock_cnt else response = success: false @@ -74,6 +76,7 @@ class StaffGrading # model state @state = state_no_data + @submission_id = null @submission = '' @rubric = '' @error_msg = '' @@ -110,7 +113,7 @@ class StaffGrading if response.success if response.submission - @data_loaded(response.submission, response.rubric) + @data_loaded(response.submission, response.rubric, response.submission_id) else @no_more(response.message) else @@ -122,7 +125,10 @@ class StaffGrading @backend.post('get_next', {}, @ajax_callback) submit_and_get_next: () -> - data = {score: @score, feedback: @feedback_area.val()} + data = + score: @score + feedback: @feedback_area.val() + submission_id: @submission_id @backend.post('save_grade', data, @ajax_callback) @@ -130,9 +136,10 @@ class StaffGrading @error_msg = msg @state = state_error - data_loaded: (submission, rubric) -> + data_loaded: (submission, rubric, submission_id) -> @submission = submission @rubric = rubric + @submission_id = submission_id @feedback_area.val('') @score = null @state = state_grading @@ -140,6 +147,7 @@ class StaffGrading no_more: (message) -> @submission = null @rubric = null + @submission_id = null @message = message @state = state_no_data @@ -196,7 +204,7 @@ class StaffGrading # for now, just create an instance and load it... -mock_backend = true +mock_backend = false ajax_url = $('.staff-grading').data('ajax_url') backend = new StaffGradingBackend(ajax_url, mock_backend) diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index d9d183c161..d8e834d4f6 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -15,7 +15,44 @@
-
- ${view_html} -
+ +
+

Staff grading

+ +
+
+ +
+
+ +
+

Submission

+
+
+
+ +
+

Rubric

+
+
+ +
+ +

+ + + + +

+
+ +
+ +
+ +
+ +
+
From a584f06bac1d03ca558491a40d9cadd9a5bf0ccf Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 18:00:48 -0500 Subject: [PATCH 095/736] Add support for varying max_score. --- .../instructor/staff_grading_service.py | 26 +++++++--- lms/djangoapps/instructor/tests.py | 16 ++---- .../src/staff_grading/staff_grading.coffee | 50 +++++++++++++------ .../src/staff_grading/test_grading.html | 6 +-- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index 2a2a7a3552..11f6189547 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -34,9 +34,10 @@ class MockStaffGradingService(object): return json.dumps({'success': True, 'submission_id': self.cnt, 'submission': 'Test submission {cnt}'.format(cnt=self.cnt), + 'max_score': 2 + self.cnt % 3, 'rubric': 'A rubric'}) - def save_grade(self, course_id, submission_id, score, feedback): + def save_grade(self, course_id, grader_id, submission_id, score, feedback): return self.get_next(course_id) @@ -62,16 +63,23 @@ class StaffGradingService(object): return r.text - def save_grade(self, course_id, submission_id, score, feedback): + def save_grade(self, course_id, grader_id, submission_id, score, feedback): """ - Save a grade. + Save a score and feedback for a submission. - TODO: what is data? + Returns json dict with keys + 'success': bool + 'error': error msg, if something went wrong. - Returns json, or raises GradingServiceError if there's a problem. + Raises GradingServiceError if there's a problem connecting. """ try: - r = self.session.get(self.url + 'save_grade') + data = {'course_id': course_id, + 'submission_id': submission_id, + 'score': score, + 'feedback': feedback, + 'grader_id': grader} + r = self.session.post(self.url + 'save_grade') except requests.exceptions.ConnectionError as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] @@ -163,7 +171,11 @@ def save_grade(request, course_id): p = request.POST try: - result_json = _service.save_grade(course_id, p['submission_id'], p['score'], p['feedback']) + result_json = _service.save_grade(course_id, + request.user.id, + p['submission_id'], + p['score'], + p['feedback']) except GradingServiceError: log.exception("Error saving grade") return _err_response('Could not connect to grading service') diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 28a56ad9de..87be93128c 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -112,15 +112,9 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader): xmodule.modulestore.django._MODULESTORES = {} courses = modulestore().get_courses() -<<<<<<< HEAD - self.full = modulestore().get_course("edX/full/6.002_Spring_2012") - self.toy = modulestore().get_course("edX/toy/2012_Fall") -======= - self.course_id = "edX/toy/2012_Fall" self.toy = modulestore().get_course(self.course_id) ->>>>>>> Refactor testing code, hook up frontend. # Create two accounts self.student = 'view@test.com' @@ -236,9 +230,6 @@ class TestStaffGradingService(ct.PageLoader): access control and error handling logic -- all the actual work is on the backend. ''' - - - def setUp(self): xmodule.modulestore.django._MODULESTORES = {} @@ -265,8 +256,6 @@ class TestStaffGradingService(ct.PageLoader): self.check_for_get_code(404, url) self.check_for_post_code(404, url) -<<<<<<< HEAD -======= @patch.object(staff_grading_service, '_service', _mock_service) def test_get_next(self): @@ -286,11 +275,12 @@ class TestStaffGradingService(ct.PageLoader): url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id}) - data = {'score': '12', 'feedback': 'great!', 'submission_id': '123'} + data = {'score': '12', + 'feedback': 'great!', + 'submission_id': '123'} r = self.check_for_post_code(200, url, data) d = json.loads(r.content) self.assertTrue(d['success'], str(d)) self.assertEquals(d['submission_id'], _mock_service.cnt) ->>>>>>> Refactor testing code, hook up frontend. diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 7440277d98..b36b9e33e7 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -18,21 +18,19 @@ class StaffGradingBackend mock: (cmd, data) -> # Return a mock response to cmd and data - @mock_cnt++ if cmd == 'get_next' + @mock_cnt++ response = success: true submission: 'submission! ' + @mock_cnt rubric: 'A rubric! ' + @mock_cnt submission_id: @mock_cnt + max_score: 2 + @mock_cnt % 3 else if cmd == 'save_grade' console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") response = - success: true - submission: 'another submission! ' + @mock_cnt - rubric: 'A rubric!' + @mock_cnt - submission_id: @mock_cnt + @mock('get_next', {}) else response = success: false @@ -72,6 +70,7 @@ class StaffGrading @submission_wrapper = $('.submission-wrapper') @rubric_wrapper = $('.rubric-wrapper') @feedback_area = $('.feedback-area') + @score_selection_container = $('.score-selection-container') @submit_button = $('.submit-button') # model state @@ -81,15 +80,12 @@ class StaffGrading @rubric = '' @error_msg = '' @message = '' + @max_score = 0 @score = null # action handlers @submit_button.click @submit - # TODO: hook up an event to the input changing, which updates - # @score (instead of the individual hacks) - $('#correct-radio').click @graded_callback - $('#incorrect-radio').click @graded_callback # render intial state @render_view() @@ -98,6 +94,24 @@ class StaffGrading @get_next_submission() + setup_score_selection: => + # first, get rid of all the old inputs, if any. + @score_selection_container.html('') + + # Now create new labels and inputs for each possible score. + for score in [1..@max_score] + id = 'score-' + score + label = """""" + + input = """ + + """ + @score_selection_container.append(label + input) + + # And now hook up an event handler again + $("input[name='score-selection']").change @graded_callback + + set_button_text: (text) => @submit_button.attr('value', text) @@ -113,7 +127,7 @@ class StaffGrading if response.success if response.submission - @data_loaded(response.submission, response.rubric, response.submission_id) + @data_loaded(response.submission, response.rubric, response.submission_id, response.max_score) else @no_more(response.message) else @@ -136,11 +150,12 @@ class StaffGrading @error_msg = msg @state = state_error - data_loaded: (submission, rubric, submission_id) -> + data_loaded: (submission, rubric, submission_id, max_score) -> @submission = submission @rubric = rubric @submission_id = submission_id @feedback_area.val('') + @max_score = max_score @score = null @state = state_grading @@ -149,6 +164,8 @@ class StaffGrading @rubric = null @submission_id = null @message = message + @score = null + @max_score = 0 @state = state_no_data render_view: () -> @@ -157,6 +174,9 @@ class StaffGrading show_submit_button = true @message_container.html(@message) + if @backend.mock_backend + @message_container.append("

NOTE: Mocking backend.

") + @error_container.html(@error_msg) if @state == state_error @@ -170,10 +190,8 @@ class StaffGrading # no submit button until user picks grade. show_submit_button = false - # TODO: clean up with proper input-related logic - $('#correct-radio')[0].checked = false - $('#incorrect-radio')[0].checked = false - + @setup_score_selection() + else if @state == state_graded show_grading_elements = true @set_button_text('Submit') @@ -204,7 +222,7 @@ class StaffGrading # for now, just create an instance and load it... -mock_backend = false +mock_backend = true ajax_url = $('.staff-grading').data('ajax_url') backend = new StaffGradingBackend(ajax_url, mock_backend) diff --git a/lms/static/coffee/src/staff_grading/test_grading.html b/lms/static/coffee/src/staff_grading/test_grading.html index 8a1cde1fd4..9b84d0703b 100644 --- a/lms/static/coffee/src/staff_grading/test_grading.html +++ b/lms/static/coffee/src/staff_grading/test_grading.html @@ -30,11 +30,7 @@
-

- - - - +

From d1b433dba57dfe60e2872e33c23ba5841abe7c99 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 19 Nov 2012 18:08:07 -0500 Subject: [PATCH 096/736] add adaptive-max-score html to lms view --- lms/static/coffee/src/staff_grading/staff_grading.coffee | 4 ++-- lms/templates/instructor/staff_grading.html | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index b36b9e33e7..a3d5cf939b 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -105,7 +105,7 @@ class StaffGrading input = """ - """ + """ # " fix broken parsing in emacs @score_selection_container.append(label + input) # And now hook up an event handler again @@ -222,7 +222,7 @@ class StaffGrading # for now, just create an instance and load it... -mock_backend = true +mock_backend = false ajax_url = $('.staff-grading').data('ajax_url') backend = new StaffGradingBackend(ajax_url, mock_backend) diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index d8e834d4f6..dbf2e97dde 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -37,13 +37,10 @@
+ -

- - - - +

From d2cc8696eda59d42eb6eb9014ac09631db87dd24 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 20 Nov 2012 11:11:18 -0500 Subject: [PATCH 097/736] Add some initial css. --- .../src/staff_grading/staff_grading.coffee | 4 +-- lms/static/sass/course.scss | 1 + lms/static/sass/course/_staff_grading.scss | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 lms/static/sass/course/_staff_grading.scss diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index a3d5cf939b..ffdb28ccfd 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -96,7 +96,7 @@ class StaffGrading setup_score_selection: => # first, get rid of all the old inputs, if any. - @score_selection_container.html('') + @score_selection_container.html('Choose score: ') # Now create new labels and inputs for each possible score. for score in [1..@max_score] @@ -106,7 +106,7 @@ class StaffGrading input = """ """ # " fix broken parsing in emacs - @score_selection_container.append(label + input) + @score_selection_container.append(input + label) # And now hook up an event handler again $("input[name='score-selection']").change @graded_callback diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss index acd735d25e..e900e589b2 100644 --- a/lms/static/sass/course.scss +++ b/lms/static/sass/course.scss @@ -43,6 +43,7 @@ @import "course/profile"; @import "course/gradebook"; @import "course/tabs"; +@import "course/staff_grading"; // instructor @import "course/instructor/instructor"; diff --git a/lms/static/sass/course/_staff_grading.scss b/lms/static/sass/course/_staff_grading.scss new file mode 100644 index 0000000000..4900f78bd0 --- /dev/null +++ b/lms/static/sass/course/_staff_grading.scss @@ -0,0 +1,29 @@ +div.staff-grading { + textarea.feedback-area { + height: 100px; + margin: 20px; + } + + div { + margin: 10px; + } + + label { + margin: 10px; + padding: 5px; + display: inline-block; + min-width: 50px; + background-color: #CCC; + text-size: 1.5em; + } + + /* Toggled State */ + input[type=radio]:checked + label { + background: #666; + color: white; + } + + input[name='score-selection'] { + display: none; + } +} From edd3277597644f7d7c509d2da18f135eb5ef9c5b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 20 Nov 2012 18:25:16 -0500 Subject: [PATCH 098/736] Tweaks to actually work with the backend. - specify mime type - right urls --- .../instructor/staff_grading_service.py | 39 ++++++++++++------- lms/envs/dev.py | 3 ++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index 11f6189547..47f483e75f 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -8,8 +8,7 @@ import requests import sys from django.conf import settings -from django.http import Http404 -from django.http import HttpResponse +from django.http import HttpResponse, Http404 from courseware.access import has_access from util.json_request import expect_json @@ -29,7 +28,7 @@ class MockStaffGradingService(object): def __init__(self): self.cnt = 0 - def get_next(self, course_id): + def get_next(self, course_id, grader_id): self.cnt += 1 return json.dumps({'success': True, 'submission_id': self.cnt, @@ -47,16 +46,20 @@ class StaffGradingService(object): """ def __init__(self, url): self.url = url + self.get_next_url = url + '/get_next_submission/' + self.save_grade_url = url + '/save_grade/' # TODO: add auth self.session = requests.session() - def get_next(self, course_id): + def get_next(self, course_id, grader_id): """ Get the next thing to grade. Returns json, or raises GradingServiceError if there's a problem. """ try: - r = self.session.get(self.url + 'get_next') + r = self.session.get(self.get_next_url, + params={'course_id': course_id, + 'grader_id': grader_id}) except requests.exceptions.ConnectionError as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] @@ -78,22 +81,24 @@ class StaffGradingService(object): 'submission_id': submission_id, 'score': score, 'feedback': feedback, - 'grader_id': grader} - r = self.session.post(self.url + 'save_grade') + 'grader_id': grader_id} + + r = self.session.post(self.save_grade_url, data=data) except requests.exceptions.ConnectionError as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] return r.text -#_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) -_service = MockStaffGradingService() +_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) +#_service = MockStaffGradingService() def _err_response(msg): """ Return a HttpResponse with a json dump with success=False, and the given error message. """ - return HttpResponse(json.dumps({'success': False, 'error': msg})) + return HttpResponse(json.dumps({'success': False, 'error': msg}), + mimetype="application/json") def _check_access(user, course_id): @@ -129,16 +134,17 @@ def get_next(request, course_id): """ _check_access(request.user, course_id) - return HttpResponse(_get_next(course_id)) + return HttpResponse(_get_next(course_id, request.user.id), + mimetype="application/json") -def _get_next(course_id): +def _get_next(course_id, grader_id): """ Implementation of get_next (also called from save_grade) -- returns a json string """ try: - return _service.get_next(course_id) + return _service.get_next(course_id, grader_id) except GradingServiceError: log.exception("Error from grading service") return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -168,11 +174,12 @@ def save_grade(request, course_id): if k not in request.POST.keys(): return _err_response('Missing required key {0}'.format(k)) + grader_id = request.user.id p = request.POST try: result_json = _service.save_grade(course_id, - request.user.id, + grader_id, p['submission_id'], p['score'], p['feedback']) @@ -183,6 +190,7 @@ def save_grade(request, course_id): try: result = json.loads(result_json) except ValueError: + log.exception("save_grade returned broken json: %s", result_json) return _err_response('Grading service returned mal-formatted data.') if not result.get('success', False): @@ -190,5 +198,6 @@ def save_grade(request, course_id): return _err_response('Grading service failed') # Ok, save_grade seemed to work. Get the next submission to grade. - return HttpResponse(_get_next(course_id)) + return HttpResponse(_get_next(course_id, grader_id), + mimetype="application/json") diff --git a/lms/envs/dev.py b/lms/envs/dev.py index bf72284425..57aeeb7bd9 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -102,6 +102,9 @@ SUBDOMAIN_BRANDING = { COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" +################################# Staff grading config ##################### + +STAFF_GRADING_BACKEND_URL = "http://127.0.0.1:3033/staff_grading" ################################ LMS Migration ################################# From 26f033b79ffe9cda25f870396f6cb645310fbab0 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 20 Nov 2012 18:25:25 -0500 Subject: [PATCH 099/736] allow score 0 --- lms/static/coffee/src/staff_grading/staff_grading.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index ffdb28ccfd..5edc5e5c35 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -99,7 +99,7 @@ class StaffGrading @score_selection_container.html('Choose score: ') # Now create new labels and inputs for each possible score. - for score in [1..@max_score] + for score in [0..@max_score] id = 'score-' + score label = """""" From b48b389a565e6dd938a0009be338bcba6d6f9632 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 26 Nov 2012 16:40:25 -0500 Subject: [PATCH 100/736] implement login into staff grading service. --- .../instructor/staff_grading_service.py | 71 ++++++++++++++++--- lms/envs/common.py | 2 + lms/envs/dev.py | 2 + 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index 47f483e75f..5bff10bcda 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -5,6 +5,7 @@ This module provides views that proxy to the staff grading backend service. import json import logging import requests +from requests.exceptions import RequestException, ConnectionError, HTTPError import sys from django.conf import settings @@ -44,23 +45,70 @@ class StaffGradingService(object): """ Interface to staff grading backend. """ - def __init__(self, url): + def __init__(self, url, username, password): + self.username = username + self.password = password self.url = url + + self.login_url = url + '/login/' self.get_next_url = url + '/get_next_submission/' self.save_grade_url = url + '/save_grade/' + # TODO: add auth self.session = requests.session() + + def _login(self): + """ + Log into the staff grading service. + + Raises requests.exceptions.HTTPError if something goes wrong. + + Returns the decoded json dict of the response. + """ + response = self.session.post(self.login_url, + {'username': self.username, + 'password': self.password,}) + + response.raise_for_status() + + return response.json + + + def _try_with_login(self, operation): + """ + Call operation(), which should return a requests response object. If + the response status code is 302, call _login() and try the operation + again. NOTE: use requests.get(..., allow_redirects=False) to have + requests not auto-follow redirects. + + Returns the result of operation(). Does not catch exceptions. + """ + response = operation() + if response.status_code == 302: + # redirect means we aren't logged in + r = self._login() + if r and not r.get('success'): + log.warning("Couldn't log into staff_grading backend. Response: %s", + r) + # try again + return operation() + + return response + + def get_next(self, course_id, grader_id): """ Get the next thing to grade. Returns json, or raises GradingServiceError if there's a problem. """ + op = lambda: self.session.get(self.get_next_url, + allow_redirects=False, + params={'course_id': course_id, + 'grader_id': grader_id}) try: - r = self.session.get(self.get_next_url, - params={'course_id': course_id, - 'grader_id': grader_id}) - except requests.exceptions.ConnectionError as err: + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] @@ -82,15 +130,20 @@ class StaffGradingService(object): 'score': score, 'feedback': feedback, 'grader_id': grader_id} - - r = self.session.post(self.save_grade_url, data=data) - except requests.exceptions.ConnectionError as err: + + op = lambda: self.session.post(self.save_grade_url, data=data, + allow_redirects=False) + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: # reraise as promised GradingServiceError, but preserve stacktrace. raise GradingServiceError, str(err), sys.exc_info()[2] return r.text -_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL) +_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL, + settings.STAFF_GRADING_BACKEND_USERNAME, + settings.STAFF_GRADING_BACKEND_PASSWORD, + ) #_service = MockStaffGradingService() def _err_response(msg): diff --git a/lms/envs/common.py b/lms/envs/common.py index 7ddd2ffb9a..005d333e09 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -325,6 +325,8 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Staff grading config ##################### STAFF_GRADING_BACKEND_URL = None +STAFF_GRADING_BACKEND_USERNAME = None +STAFF_GRADING_BACKEND_PASSWORD = None ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 57aeeb7bd9..f3cc3e4c63 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -105,6 +105,8 @@ COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" ################################# Staff grading config ##################### STAFF_GRADING_BACKEND_URL = "http://127.0.0.1:3033/staff_grading" +STAFF_GRADING_BACKEND_USERNAME = "lms" +STAFF_GRADING_BACKEND_PASSWORD = "abcd" ################################ LMS Migration ################################# From d28cd4f42910852e3242091901301b5f20bf9225 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 26 Nov 2012 17:09:52 -0500 Subject: [PATCH 101/736] use a dict for backend config. Load it on aws. --- .../instructor/staff_grading_service.py | 19 ++++++++----------- lms/envs/aws.py | 3 +++ lms/envs/common.py | 4 +--- lms/envs/dev.py | 11 ++++++----- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index 5bff10bcda..05c131ed56 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -45,14 +45,14 @@ class StaffGradingService(object): """ Interface to staff grading backend. """ - def __init__(self, url, username, password): - self.username = username - self.password = password - self.url = url + def __init__(self, config): + self.username = config['username'] + self.password = config['password'] + self.url = config['url'] - self.login_url = url + '/login/' - self.get_next_url = url + '/get_next_submission/' - self.save_grade_url = url + '/save_grade/' + self.login_url = self.url + '/login/' + self.get_next_url = self.url + '/get_next_submission/' + self.save_grade_url = self.url + '/save_grade/' # TODO: add auth self.session = requests.session() @@ -140,10 +140,7 @@ class StaffGradingService(object): return r.text -_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL, - settings.STAFF_GRADING_BACKEND_USERNAME, - settings.STAFF_GRADING_BACKEND_PASSWORD, - ) +_service = StaffGradingService(settings.STAFF_GRADING_INTERFACE) #_service = MockStaffGradingService() def _err_response(msg): diff --git a/lms/envs/aws.py b/lms/envs/aws.py index b58bc5602b..d1abce8a6d 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -76,5 +76,8 @@ DATABASES = AUTH_TOKENS['DATABASES'] XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE'] +STAFF_GRADING_BACKEND = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE') + + PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") diff --git a/lms/envs/common.py b/lms/envs/common.py index 005d333e09..3d26cb54c9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -324,9 +324,7 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Staff grading config ##################### -STAFF_GRADING_BACKEND_URL = None -STAFF_GRADING_BACKEND_USERNAME = None -STAFF_GRADING_BACKEND_PASSWORD = None +STAFF_GRADING_INTERFACE = None ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' diff --git a/lms/envs/dev.py b/lms/envs/dev.py index f3cc3e4c63..0ad42f67d3 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -39,7 +39,7 @@ DATABASES = { } CACHES = { - # This is the cache used for most things. + # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -104,10 +104,11 @@ COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" ################################# Staff grading config ##################### -STAFF_GRADING_BACKEND_URL = "http://127.0.0.1:3033/staff_grading" -STAFF_GRADING_BACKEND_USERNAME = "lms" -STAFF_GRADING_BACKEND_PASSWORD = "abcd" - +STAFF_GRADING_INTERFACE = { + 'url': 'http://127.0.0.1:3033/staff_grading', + 'username': 'lms', + 'password': 'abcd', + } ################################ LMS Migration ################################# MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True From 835f18795afe56f3fd4e6b96935f705d103f0ad2 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 30 Nov 2012 10:27:34 -0500 Subject: [PATCH 102/736] Make tests pass again --- .../instructor/staff_grading_service.py | 30 +++++++++++++++---- lms/djangoapps/instructor/tests.py | 16 +++++++--- lms/envs/common.py | 3 ++ lms/envs/test.py | 6 +++- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index 05c131ed56..c070bd6835 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -38,7 +38,7 @@ class MockStaffGradingService(object): 'rubric': 'A rubric'}) def save_grade(self, course_id, grader_id, submission_id, score, feedback): - return self.get_next(course_id) + return self.get_next(course_id, grader_id) class StaffGradingService(object): @@ -140,8 +140,28 @@ class StaffGradingService(object): return r.text -_service = StaffGradingService(settings.STAFF_GRADING_INTERFACE) -#_service = MockStaffGradingService() +# don't initialize until grading_service() is called--means that just +# importing this file doesn't create objects that may not have the right config +_service = None + +def grading_service(): + """ + Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True, + returns a mock one, otherwise a real one. + + Caches the result, so changing the setting after the first call to this + function will have no effect. + """ + global _service + if _service is not None: + return _service + + if settings.MOCK_STAFF_GRADING: + _service = MockStaffGradingService() + else: + _service = StaffGradingService(settings.STAFF_GRADING_INTERFACE) + + return _service def _err_response(msg): """ @@ -194,7 +214,7 @@ def _get_next(course_id, grader_id): """ try: - return _service.get_next(course_id, grader_id) + return grading_service().get_next(course_id, grader_id) except GradingServiceError: log.exception("Error from grading service") return json.dumps({'success': False, 'error': 'Could not connect to grading service'}) @@ -228,7 +248,7 @@ def save_grade(request, course_id): p = request.POST try: - result_json = _service.save_grade(course_id, + result_json = grading_service().save_grade(course_id, grader_id, p['submission_id'], p['score'], diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 87be93128c..c47eb170fc 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -233,6 +233,14 @@ class TestStaffGradingService(ct.PageLoader): def setUp(self): xmodule.modulestore.django._MODULESTORES = {} + self.student = 'view@test.com' + self.instructor = 'view2@test.com' + self.password = 'foo' + self.create_account('u1', self.student, self.password) + self.create_account('u2', self.instructor, self.password) + self.activate_user(self.student) + self.activate_user(self.instructor) + self.course_id = "edX/toy/2012_Fall" self.toy = modulestore().get_course(self.course_id) def make_instructor(course): @@ -242,6 +250,8 @@ class TestStaffGradingService(ct.PageLoader): make_instructor(self.toy) + self.mock_service = staff_grading_service.grading_service() + self.logout() def test_access(self): @@ -257,7 +267,6 @@ class TestStaffGradingService(ct.PageLoader): self.check_for_post_code(404, url) - @patch.object(staff_grading_service, '_service', _mock_service) def test_get_next(self): self.login(self.instructor, self.password) @@ -266,10 +275,9 @@ class TestStaffGradingService(ct.PageLoader): r = self.check_for_get_code(200, url) d = json.loads(r.content) self.assertTrue(d['success']) - self.assertEquals(d['submission_id'], _mock_service.cnt) + self.assertEquals(d['submission_id'], self.mock_service.cnt) - @patch.object(staff_grading_service, '_service', _mock_service) def test_save_grade(self): self.login(self.instructor, self.password) @@ -281,6 +289,6 @@ class TestStaffGradingService(ct.PageLoader): r = self.check_for_post_code(200, url, data) d = json.loads(r.content) self.assertTrue(d['success'], str(d)) - self.assertEquals(d['submission_id'], _mock_service.cnt) + self.assertEquals(d['submission_id'], self.mock_service.cnt) diff --git a/lms/envs/common.py b/lms/envs/common.py index 3d26cb54c9..79d0bb78f9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -325,6 +325,9 @@ WIKI_LINK_DEFAULT_LEVEL = 2 ################################# Staff grading config ##################### STAFF_GRADING_INTERFACE = None +# Used for testing, debugging +MOCK_STAFF_GRADING = False + ################################# Jasmine ################################### JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' diff --git a/lms/envs/test.py b/lms/envs/test.py index e815efdf4e..ef2a343db4 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -65,6 +65,10 @@ XQUEUE_INTERFACE = { } XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds + +# Don't rely on a real staff grading backend +MOCK_STAFF_GRADING = True + # TODO (cpennington): We need to figure out how envs/test.py can inject things # into common.py so that we don't have to repeat this sort of thing STATICFILES_DIRS = [ @@ -99,7 +103,7 @@ DATABASES = { } CACHES = { - # This is the cache used for most things. + # This is the cache used for most things. # In staging/prod envs, the sessions also live here. 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', From 052807d7f60a877e6924d1240c8f481a23803ba5 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 30 Nov 2012 11:30:02 -0500 Subject: [PATCH 103/736] Change the unique student id calculation to match the id we send to xqueue - can't change the xqueue one because there's data that uses it - we checked, and no one has sent out survey links that use the old sha1() format doing this because we'll need the LMS to be able to send the anon student id to backend services e.g. for peer grading, and passing two different anon student ids to xmodule (one for xqueue, one for other uses) is just asking for confusion. --- common/djangoapps/student/models.py | 15 +++++++-------- lms/djangoapps/courseware/module_render.py | 10 ++-------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 5975853a21..2f5bc3ac04 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -36,7 +36,7 @@ file and check it in at the same time as your model changes. To do that, 3. Add the migration file created in mitx/common/djangoapps/student/migrations/ """ from datetime import datetime -from hashlib import sha1 +import hashlib import json import logging import uuid @@ -197,14 +197,13 @@ def unique_id_for_user(user): """ Return a unique id for a user, suitable for inserting into e.g. personalized survey links. - - Currently happens to be implemented as a sha1 hash of the username - (and thus assumes that usernames don't change). """ - # Using the user id as the salt because it's sort of random, and is already - # in the db. - salt = str(user.id) - return sha1(salt + user.username).hexdigest() + # include the secret key as a salt, and to make the ids unique accross + # different LMS installs. + h = hashlib.md5() + h.update(settings.SECRET_KEY) + h.update(str(user.id)) + return h.hexdigest() ## TODO: Should be renamed to generic UserGroup, and possibly diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 67927c0ee7..eb7b41b1e9 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,4 +1,3 @@ -import hashlib import json import logging import pyparsing @@ -20,6 +19,7 @@ from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from static_replace import replace_urls +from student.models import unique_id_for_user from xmodule.errortracker import exc_info_to_str from xmodule.exceptions import NotFoundError from xmodule.modulestore import Location @@ -152,12 +152,6 @@ def _get_module(user, request, location, student_module_cache, course_id, positi if not has_access(user, descriptor, 'load'): return None - # Anonymized student identifier - h = hashlib.md5() - h.update(settings.SECRET_KEY) - h.update(str(user.id)) - anonymous_student_id = h.hexdigest() - # Only check the cache if this module can possibly have state instance_module = None shared_module = None @@ -230,7 +224,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi # by the replace_static_urls code below replace_urls=replace_urls, node_path=settings.NODE_PATH, - anonymous_student_id=anonymous_student_id, + anonymous_student_id=unique_id_for_user(user), ) # pass position specified in URL to module through ModuleSystem system.set('position', position) From 51e148a7a7413176afd8661d2a8defa25bd729c4 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 11:42:24 -0500 Subject: [PATCH 104/736] Add in support for rendering feedback within lms --- common/lib/capa/capa/responsetypes.py | 46 ++++++++++++++++++++++++-- lms/templates/open_ended_error.html | 12 +++++++ lms/templates/open_ended_feedback.html | 16 +++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 lms/templates/open_ended_error.html create mode 100644 lms/templates/open_ended_feedback.html diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index ba9f03549e..8bdfb4f550 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -24,6 +24,7 @@ import os import subprocess import xml.sax.saxutils as saxutils from shapely.geometry import Point, MultiPoint +from django.template.loader import render_to_string # specific library imports from calc import evaluator, UndefinedVariable @@ -1965,6 +1966,8 @@ class OpenEndedResponse(LoncapaResponse): 'max_score' : self.max_score }) + log.debug(xheader) + log.debug(contents) # Submit request. When successful, 'msg' is the prior length of the queue (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) @@ -2027,6 +2030,40 @@ class OpenEndedResponse(LoncapaResponse): def get_initial_display(self): return {self.answer_id: self.initial_display} + def _format_feedback(self, response_items): + """ + Input: + Dictionary called feedback. Must contain keys seen below. + Output: + Return success/fail, error message or feedback template + """ + tags=['feedback_items','score','grader_type'] + + for tag in tags: + if tag not in response_items: + return False, "Grader response missing required feedback key!" + + if 'errors' in response_items['feedback_items']: + return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback_items']['errors']}) + + + feedback_item_start='
' + feedback_item_end='
' + feedback_long="" + for k,v in response_items: + feedback_long+=feedback_item_start.format(feedback_key=k) + feedback_long+=v + feedback_long+=feedback_item_end + + feedback_template=render_to_string("open_ended_feedback.html",{ + 'grader_type' : response_items['grader_type'], + 'score' : response_items['score'], + 'feedback_long' : feedback_long, + }) + + return True, feedback_template + + def _parse_score_msg(self, score_msg): """ Grader reply is a JSON-dump of the following dict @@ -2052,7 +2089,7 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['score','feedback']: + for tag in ['score','feedback', 'grader_type']: if tag not in score_result: log.error("External grader message is missing one or more required" " tags: 'score', 'feedback") @@ -2062,7 +2099,12 @@ class OpenEndedResponse(LoncapaResponse): # is safe for the LMS. # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? - feedback = score_result['feedback'] + + feedback = self._format_feedback({ + 'score' : score_result['score'], + 'feedback_items' : score_result['feedback'], + 'grader_type' : score_result['grader_type'], + }) score_ratio=int(score_result['score'])/self.max_score diff --git a/lms/templates/open_ended_error.html b/lms/templates/open_ended_error.html new file mode 100644 index 0000000000..c91beb88b0 --- /dev/null +++ b/lms/templates/open_ended_error.html @@ -0,0 +1,12 @@ +
+
+
+ There was an error with your submission. Please contact course staff. +
+
+
+
+ {errors} +
+
+
\ No newline at end of file diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html new file mode 100644 index 0000000000..39de3ad1c3 --- /dev/null +++ b/lms/templates/open_ended_feedback.html @@ -0,0 +1,16 @@ +
+
Feedback
+
+
+

Score: ${score}

+ % if grader_type=="ML" +

Number of potential problem areas identified: ${problem_areas}

+ % endif +
+
+
+
+ ${feedback_long} +
+
+
\ No newline at end of file From 15c5072984b5e1bceed2ff1eace82380c433e00f Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 11:47:12 -0500 Subject: [PATCH 105/736] Work on template render code --- common/lib/capa/capa/responsetypes.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 8bdfb4f550..c587034931 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2037,20 +2037,15 @@ class OpenEndedResponse(LoncapaResponse): Output: Return success/fail, error message or feedback template """ - tags=['feedback_items','score','grader_type'] - for tag in tags: - if tag not in response_items: - return False, "Grader response missing required feedback key!" - - if 'errors' in response_items['feedback_items']: - return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback_items']['errors']}) + if not response_items['success']: + return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback']['errors']}) feedback_item_start='
' feedback_item_end='
' feedback_long="" - for k,v in response_items: + for k,v in response_items['feedback']: feedback_long+=feedback_item_start.format(feedback_key=k) feedback_long+=v feedback_long+=feedback_item_end @@ -2089,7 +2084,7 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['score','feedback', 'grader_type']: + for tag in ['score','feedback', 'grader_type', 'success', 'errors']: if tag not in score_result: log.error("External grader message is missing one or more required" " tags: 'score', 'feedback") @@ -2100,11 +2095,7 @@ class OpenEndedResponse(LoncapaResponse): # 1) Make sure that the message is valid XML (proper opening/closing tags) # 2) TODO: Is the message actually HTML? - feedback = self._format_feedback({ - 'score' : score_result['score'], - 'feedback_items' : score_result['feedback'], - 'grader_type' : score_result['grader_type'], - }) + feedback = self._format_feedback(score_result) score_ratio=int(score_result['score'])/self.max_score From 7330bef8e6e727708525afa133b93777cb55e02a Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 12:11:58 -0500 Subject: [PATCH 106/736] Changes to fix compatibility with controller feedback --- common/lib/capa/capa/responsetypes.py | 19 +++++++------------ lms/templates/open_ended_feedback.html | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index c587034931..42f3091e6f 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2039,21 +2039,17 @@ class OpenEndedResponse(LoncapaResponse): """ if not response_items['success']: - return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback']['errors']}) + return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback']}) + problem_areas=response_items['feedback'].count("") - feedback_item_start='
' - feedback_item_end='
' - feedback_long="" - for k,v in response_items['feedback']: - feedback_long+=feedback_item_start.format(feedback_key=k) - feedback_long+=v - feedback_long+=feedback_item_end + feedback=response_items['feedback'] feedback_template=render_to_string("open_ended_feedback.html",{ 'grader_type' : response_items['grader_type'], 'score' : response_items['score'], - 'feedback_long' : feedback_long, + 'feedback' : feedback, + 'problem_areas' : problem_areas, }) return True, feedback_template @@ -2084,10 +2080,9 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['score','feedback', 'grader_type', 'success', 'errors']: + for tag in ['score', 'feedback', 'grader_type', 'success', 'errors']: if tag not in score_result: - log.error("External grader message is missing one or more required" - " tags: 'score', 'feedback") + log.error("External grader message is missing required tag: {0}".format(tag)) return fail # Next, we need to check that the contents of the external grader message diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html index 39de3ad1c3..91efc20958 100644 --- a/lms/templates/open_ended_feedback.html +++ b/lms/templates/open_ended_feedback.html @@ -10,7 +10,7 @@
- ${feedback_long} + ${feedback}
\ No newline at end of file From 2a0c9d0c705091403661c74db79476964c7d2b47 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 12:48:13 -0500 Subject: [PATCH 107/736] Handle templating within lms --- common/lib/capa/capa/responsetypes.py | 11 +++++------ lms/templates/open_ended_error.html | 2 +- lms/templates/open_ended_feedback.html | 10 +++++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 42f3091e6f..7eff89fbb4 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2035,13 +2035,12 @@ class OpenEndedResponse(LoncapaResponse): Input: Dictionary called feedback. Must contain keys seen below. Output: - Return success/fail, error message or feedback template + Return error message or feedback template """ if not response_items['success']: - return True, render_to_string("open_ended_error.html", {'errors' : response_items['feedback']}) + return render_to_string("open_ended_error.html", {'errors' : response_items['feedback']}) - problem_areas=response_items['feedback'].count("") feedback=response_items['feedback'] @@ -2049,10 +2048,9 @@ class OpenEndedResponse(LoncapaResponse): 'grader_type' : response_items['grader_type'], 'score' : response_items['score'], 'feedback' : feedback, - 'problem_areas' : problem_areas, }) - return True, feedback_template + return feedback_template def _parse_score_msg(self, score_msg): @@ -2080,7 +2078,7 @@ class OpenEndedResponse(LoncapaResponse): log.error("External grader message should be a JSON-serialized dict." " Received score_result = %s" % score_result) return fail - for tag in ['score', 'feedback', 'grader_type', 'success', 'errors']: + for tag in ['score', 'feedback', 'grader_type', 'success']: if tag not in score_result: log.error("External grader message is missing required tag: {0}".format(tag)) return fail @@ -2098,6 +2096,7 @@ class OpenEndedResponse(LoncapaResponse): if score_ratio>=.66: correct=True + log.debug(feedback) try: etree.fromstring(feedback) except etree.XMLSyntaxError as err: diff --git a/lms/templates/open_ended_error.html b/lms/templates/open_ended_error.html index c91beb88b0..fb3698ccd3 100644 --- a/lms/templates/open_ended_error.html +++ b/lms/templates/open_ended_error.html @@ -6,7 +6,7 @@
- {errors} + {{errors}}
\ No newline at end of file diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html index 91efc20958..1e59652d33 100644 --- a/lms/templates/open_ended_feedback.html +++ b/lms/templates/open_ended_feedback.html @@ -2,15 +2,15 @@
Feedback
-

Score: ${score}

- % if grader_type=="ML" -

Number of potential problem areas identified: ${problem_areas}

- % endif +

Score: {{score}}

+ {% if grader_type == "ML" %} +

Check below for full feedback:

+ {% endif %}
- ${feedback} + {% autoescape off %} {{feedback |safe }} {% endautoescape %}
\ No newline at end of file From 9fb6c6f9891006f5372c3bd46e83add9b67f0c09 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 13:18:32 -0500 Subject: [PATCH 108/736] Debug template issues --- lms/templates/open_ended_feedback.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html index 1e59652d33..2dc8f555f6 100644 --- a/lms/templates/open_ended_feedback.html +++ b/lms/templates/open_ended_feedback.html @@ -10,7 +10,7 @@
- {% autoescape off %} {{feedback |safe }} {% endautoescape %} + {% autoescape off %} {{feedback}} {% endautoescape %}
\ No newline at end of file From 2621d07ced5904b0c52882c5d0d7af0329c7c9ca Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 13:48:17 -0500 Subject: [PATCH 109/736] Remove unneeded debug statements --- common/lib/capa/capa/responsetypes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 7eff89fbb4..aec90627f2 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1966,8 +1966,6 @@ class OpenEndedResponse(LoncapaResponse): 'max_score' : self.max_score }) - log.debug(xheader) - log.debug(contents) # Submit request. When successful, 'msg' is the prior length of the queue (error, msg) = qinterface.send_to_queue(header=xheader, body=json.dumps(contents)) From 32d8a6e92275dbbee7bbda26625b4beb46896a4a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 30 Nov 2012 14:11:11 -0500 Subject: [PATCH 110/736] Pass through course_id, location to openendedresponse - set default queue in response type--don't use the default per-course one --- common/lib/capa/capa/responsetypes.py | 27 +++++++++++++--------- common/lib/xmodule/xmodule/capa_module.py | 5 ++++ common/lib/xmodule/xmodule/x_module.py | 6 ++++- lms/djangoapps/courseware/module_render.py | 1 + 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index ba9f03549e..6346b08a42 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1730,9 +1730,9 @@ class ImageResponse(LoncapaResponse): Regions is list of lists [region1, region2, region3, ...] where regionN is disordered list of points: [[1,1], [100,100], [50,50], [20, 70]]. - + If there is only one region in the list, simpler notation can be used: - regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly + regions="[[10,10], [30,30], [10, 30], [30, 10]]" (without explicitly setting outer list) Returns: @@ -1817,19 +1817,24 @@ class ImageResponse(LoncapaResponse): class OpenEndedResponse(LoncapaResponse): """ - Grade student open ended responses using an external queueing server, called 'xqueue' + Grade student open ended responses using an external grading system, + accessed through the xqueue system. + + Expects 'xqueue' dict in ModuleSystem with the following keys that are + needed by OpenEndedResponse: - Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by OpenEndedResponse: system.xqueue = { 'interface': XqueueInterface object, 'callback_url': Per-StudentModule callback URL where results are posted (string), - 'default_queuename': Default queuename to submit request (string) } External requests are only submitted for student submission grading (i.e. and not for getting reference answers) + + By default, uses the OpenEndedResponse.DEFAULT_QUEUE queue. """ + DEFAULT_QUEUE = 'open-ended' response_tag = 'openendedresponse' allowed_inputfields = ['openendedinput'] max_inputfields = 1 @@ -1841,7 +1846,7 @@ class OpenEndedResponse(LoncapaResponse): xml = self.xml # TODO: XML can override external resource (grader/queue) URL self.url = xml.get('url', None) - self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) + self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE) #Look for tag named openendedparam that encapsulates all grader settings oeparam = self.xml.find('openendedparam') @@ -1883,11 +1888,12 @@ class OpenEndedResponse(LoncapaResponse): #Update grader payload with student id. If grader payload not json, error. try: grader_payload=json.loads(grader_payload) - location=self.system.ajax_url.split("://")[1] - org,course,type,name=location.split("/") + # NOTE: self.system.location is valid because the capa_module + # __init__ adds it (easiest way to get problem location into + # response types) grader_payload.update({ - 'location' : location, - 'course_id' : "{0}/{1}".format(org,course), + 'location' : self.system.location, + 'course_id' : self.system.course_id, 'prompt' : prompt_string, 'rubric' : rubric_string, }) @@ -1997,7 +2003,6 @@ class OpenEndedResponse(LoncapaResponse): msg='Invalid grader reply. Please contact the course staff.') return oldcmap - correctness = 'correct' if correct else 'incorrect' # TODO: Find out how this is used elsewhere, if any diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 47d5d5c423..ead138a225 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -146,6 +146,11 @@ class CapaModule(XModule): else: self.seed = None + # Need the problem location in openendedresponse to send out. Adding + # it to the system here seems like the least clunky way to get it + # there. + self.system.set('location', self.location) + try: # TODO (vshnayder): move as much as possible of this work and error # checking to descriptor load time diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 6f3fb73356..19a592191e 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -809,7 +809,8 @@ class ModuleSystem(object): debug=False, xqueue=None, node_path="", - anonymous_student_id=''): + anonymous_student_id='', + course_id=None): ''' Create a closure around the system environment. @@ -844,6 +845,8 @@ class ModuleSystem(object): ajax results. anonymous_student_id - Used for tracking modules with student id + + course_id - the course_id containing this module ''' self.ajax_url = ajax_url self.xqueue = xqueue @@ -856,6 +859,7 @@ class ModuleSystem(object): self.replace_urls = replace_urls self.node_path = node_path self.anonymous_student_id = anonymous_student_id + self.course_id = course_id self.user_is_staff = user is not None and user.is_staff def get(self, attr): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index eb7b41b1e9..bd919eeb15 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -225,6 +225,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi replace_urls=replace_urls, node_path=settings.NODE_PATH, anonymous_student_id=unique_id_for_user(user), + course_id=course_id, ) # pass position specified in URL to module through ModuleSystem system.set('position', position) From a20a6c8fb59ec755521857458dc2096cae9d911e Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 14:11:43 -0500 Subject: [PATCH 111/736] Do all html rendering and generation in lms --- common/lib/capa/capa/responsetypes.py | 47 ++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index aec90627f2..ec3f4763d5 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2028,6 +2028,46 @@ class OpenEndedResponse(LoncapaResponse): def get_initial_display(self): return {self.answer_id: self.initial_display} + def _convert_longform_feedback_to_html(response_items): + """ + Take in a dictionary, and return html formatted strings appropriate for sending via xqueue. + Input: + Dictionary with keys success, feedback, and errors + Output: + String + """ + + feedback_item_start='
' + feedback_item_end='
' + + for tag in ['status', 'feedback']: + if tag not in response_items: + feedback_long=feedback_item_start.format(feedback_key="errors") + "Error getting feedback." + feedback_item_end + + feedback_items=response_items['feedback'] + try: + feedback_items=json.loads(feedback_items) + except: + pass + + success=response_items['success'] + + if success: + feedback_long="" + for k,v in feedback_items.items(): + feedback_long+=feedback_item_start.format(feedback_key=k) + feedback_long+=str(v) + feedback_long+=feedback_item_end + + if len(feedback_items)==0: + feedback_long=feedback_item_start.format(feedback_key="feedback") + "No feedback available." + feedback_item_end + + else: + feedback_long=feedback_item_start.format(feedback_key="errors") + response_items['feedback'] + feedback_item_end + + return feedback_long + + def _format_feedback(self, response_items): """ Input: @@ -2036,11 +2076,10 @@ class OpenEndedResponse(LoncapaResponse): Return error message or feedback template """ + feedback=self._convert_longform_feedback_to_html(response_items) + if not response_items['success']: - return render_to_string("open_ended_error.html", {'errors' : response_items['feedback']}) - - - feedback=response_items['feedback'] + return render_to_string("open_ended_error.html", {'errors' : feedback}) feedback_template=render_to_string("open_ended_feedback.html",{ 'grader_type' : response_items['grader_type'], From 6e75584d06f7954cf21abf4f9ef7edf9f6d497ee Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 14:14:29 -0500 Subject: [PATCH 112/736] Convert templates to mako --- common/lib/capa/capa/responsetypes.py | 5 ++--- lms/templates/open_ended_error.html | 2 +- lms/templates/open_ended_feedback.html | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index ec3f4763d5..56be2ae584 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -24,7 +24,6 @@ import os import subprocess import xml.sax.saxutils as saxutils from shapely.geometry import Point, MultiPoint -from django.template.loader import render_to_string # specific library imports from calc import evaluator, UndefinedVariable @@ -2079,9 +2078,9 @@ class OpenEndedResponse(LoncapaResponse): feedback=self._convert_longform_feedback_to_html(response_items) if not response_items['success']: - return render_to_string("open_ended_error.html", {'errors' : feedback}) + return self.system.render_template("open_ended_error.html", {'errors' : feedback}) - feedback_template=render_to_string("open_ended_feedback.html",{ + feedback_template=self.system.render_template("open_ended_feedback.html",{ 'grader_type' : response_items['grader_type'], 'score' : response_items['score'], 'feedback' : feedback, diff --git a/lms/templates/open_ended_error.html b/lms/templates/open_ended_error.html index fb3698ccd3..58a90f86ef 100644 --- a/lms/templates/open_ended_error.html +++ b/lms/templates/open_ended_error.html @@ -6,7 +6,7 @@
- {{errors}} + ${errors}
\ No newline at end of file diff --git a/lms/templates/open_ended_feedback.html b/lms/templates/open_ended_feedback.html index 2dc8f555f6..cb90006456 100644 --- a/lms/templates/open_ended_feedback.html +++ b/lms/templates/open_ended_feedback.html @@ -2,15 +2,15 @@
Feedback
-

Score: {{score}}

- {% if grader_type == "ML" %} +

Score: ${score}

+ % if grader_type == "ML":

Check below for full feedback:

- {% endif %} + % endif
- {% autoescape off %} {{feedback}} {% endautoescape %} + ${ feedback | n}
\ No newline at end of file From 3fa1cfd010b5e2f5573d074da92c2b5de60449d2 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 14:16:30 -0500 Subject: [PATCH 113/736] Minor bugfix --- common/lib/capa/capa/responsetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 56be2ae584..451a863573 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -2027,7 +2027,7 @@ class OpenEndedResponse(LoncapaResponse): def get_initial_display(self): return {self.answer_id: self.initial_display} - def _convert_longform_feedback_to_html(response_items): + def _convert_longform_feedback_to_html(self,response_items): """ Take in a dictionary, and return html formatted strings appropriate for sending via xqueue. Input: From bcd30223208177f6cfdd3ae2b41f6ef75e3a39fc Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 15:26:52 -0500 Subject: [PATCH 114/736] Add in import for reverse in staff grading (wouldn't work without it) --- common/lib/capa/capa/responsetypes.py | 2 -- lms/djangoapps/instructor/views.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 451a863573..f387a20bb5 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1921,8 +1921,6 @@ class OpenEndedResponse(LoncapaResponse): else: self.max_score = 1 - log.debug(self.max_score) - def get_score(self, student_answers): try: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 389a64721a..79cf0caaf3 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -12,6 +12,7 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response +from django.core.urlresolvers import reverse from courseware import grades from courseware.access import has_access, get_access_group_name From 4e4bd4aba275873de94297a54f04a77be53591f0 Mon Sep 17 00:00:00 2001 From: Vik Paruchuri Date: Fri, 30 Nov 2012 15:52:11 -0500 Subject: [PATCH 115/736] Altered staff grading to display message about ml grading performance. --- .../coffee/src/staff_grading/staff_grading.coffee | 13 ++++++++++--- lms/templates/instructor/staff_grading.html | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index 5edc5e5c35..c953b38c2e 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -26,6 +26,7 @@ class StaffGradingBackend rubric: 'A rubric! ' + @mock_cnt submission_id: @mock_cnt max_score: 2 + @mock_cnt % 3 + ml_error_info : 'ML error info!' + @mock_cnt else if cmd == 'save_grade' console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") @@ -71,7 +72,8 @@ class StaffGrading @rubric_wrapper = $('.rubric-wrapper') @feedback_area = $('.feedback-area') @score_selection_container = $('.score-selection-container') - @submit_button = $('.submit-button') + @submit_button = $('.submit-button') + @ml_error_info_container = $('.ml-error-info-container') # model state @state = state_no_data @@ -81,6 +83,7 @@ class StaffGrading @error_msg = '' @message = '' @max_score = 0 + @ml_error_info= '' @score = null @@ -127,7 +130,7 @@ class StaffGrading if response.success if response.submission - @data_loaded(response.submission, response.rubric, response.submission_id, response.max_score) + @data_loaded(response.submission, response.rubric, response.submission_id, response.max_score, response.ml_error_info) else @no_more(response.message) else @@ -150,18 +153,20 @@ class StaffGrading @error_msg = msg @state = state_error - data_loaded: (submission, rubric, submission_id, max_score) -> + data_loaded: (submission, rubric, submission_id, max_score, ml_error_info) -> @submission = submission @rubric = rubric @submission_id = submission_id @feedback_area.val('') @max_score = max_score @score = null + @ml_error_info=ml_error_info @state = state_grading no_more: (message) -> @submission = null @rubric = null + @ml_error_info = null @submission_id = null @message = message @score = null @@ -183,6 +188,7 @@ class StaffGrading @set_button_text('Try loading again') else if @state == state_grading + @ml_error_info_container.html(@ml_error_info) @submission_container.html(@submission) @rubric_container.html(@rubric) show_grading_elements = true @@ -206,6 +212,7 @@ class StaffGrading @submit_button.toggle(show_submit_button) @submission_wrapper.toggle(show_grading_elements) @rubric_wrapper.toggle(show_grading_elements) + @ml_error_info_container.toggle(show_grading_elements) submit: (event) => diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index dbf2e97dde..ad704ae5a4 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -25,6 +25,9 @@
+
+
+

Submission

From 0b3d739646a770d13f412c8a7078a1b7bccfd6ac Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 30 Nov 2012 17:01:30 -0500 Subject: [PATCH 116/736] Switch to S3 hosted mathjax (I thought we did this ages ago). --- common/templates/mathjax_include.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html index 31a5358ece..a5a63e70ed 100644 --- a/common/templates/mathjax_include.html +++ b/common/templates/mathjax_include.html @@ -33,4 +33,4 @@ - + From c2c016c58b19646d90aea65dfe12753f0fad1e82 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 30 Nov 2012 18:00:45 -0500 Subject: [PATCH 117/736] Fix MathJax S3 URL to use the standard HTTPS location --- common/templates/mathjax_include.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html index a5a63e70ed..803f2145a4 100644 --- a/common/templates/mathjax_include.html +++ b/common/templates/mathjax_include.html @@ -33,4 +33,4 @@ - + From 7ffb30d0c995e86de8e947862247bb9abf5c0de7 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 30 Nov 2012 18:22:37 -0500 Subject: [PATCH 118/736] IE errors if you try to use 'return' as a dict key. --- lms/templates/courseware/xqa_interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/courseware/xqa_interface.html b/lms/templates/courseware/xqa_interface.html index 73f7cc6f52..c314cc7fb0 100644 --- a/lms/templates/courseware/xqa_interface.html +++ b/lms/templates/courseware/xqa_interface.html @@ -14,7 +14,7 @@ function sendlog(element_id, edit_link, staff_context){ location: staff_context.location, category : staff_context.category, 'username' : staff_context.user.username, - return : 'query', + 'return' : 'query', format : 'html', email : staff_context.user.email, tag:$('#' + element_id + '_xqa_tag').val(), From 0c1ebd8dfb3a10f0b6321d72c38adca02de28e1a Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Fri, 30 Nov 2012 19:05:26 -0500 Subject: [PATCH 119/736] add toylab test data and test changes, including fix to jump_to --- common/lib/xmodule/xmodule/html_module.py | 4 +- common/test/data/toy/html/secret/toylab.html | 4 + lms/djangoapps/courseware/tests/tests.py | 101 +++++++++++++++---- lms/djangoapps/courseware/views.py | 17 ++-- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 4f10cc84f1..eea747e332 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -30,7 +30,9 @@ class HtmlModule(XModule): def get_html(self): # cdodge: perform link substitutions for any references to course static content (e.g. images) - return rewrite_links(self.html, self.rewrite_content_links) + output = rewrite_links(self.html, self.rewrite_content_links) + log.info(' HTMLModule converting markup "{0}" to "{1}"'.format(self.html, output)) + return output def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/common/test/data/toy/html/secret/toylab.html b/common/test/data/toy/html/secret/toylab.html index 760482c4a0..b2a4599cc6 100644 --- a/common/test/data/toy/html/secret/toylab.html +++ b/common/test/data/toy/html/secret/toylab.html @@ -1,5 +1,6 @@ Lab 2A: Superposition Experiment +<<<<<<< Updated upstream

Isn't the toy course great?

Let's add some markup that uses non-ascii characters. @@ -7,3 +8,6 @@ For example, we should be able to write words like encyclopædia, or foreig Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π.

+======= +

Isn't the toy course great? — ≤

+>>>>>>> Stashed changes diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 8239eadfd9..defbf426cc 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -1,21 +1,15 @@ -import copy import json import os -import sys import time from nose import SkipTest -from path import path -from pprint import pprint from urlparse import urlsplit, urlunsplit from django.contrib.auth.models import User, Group -from django.core.handlers.wsgi import WSGIRequest from django.test import TestCase -from django.test.client import Client, RequestFactory +from django.test.client import RequestFactory from django.conf import settings from django.core.urlresolvers import reverse -from mock import patch, Mock from override_settings import override_settings import xmodule.modulestore.django @@ -29,6 +23,7 @@ from student.models import Registration from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml +from xmodule.modulestore.xml import XMLModuleStore from xmodule.timeparse import stringify_time def parse_json(response): @@ -76,10 +71,21 @@ def xml_store_config(data_dir): } } +def my_xml_store_config(data_dir): + return { + 'default': { + 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', + 'OPTIONS': { + 'data_dir': data_dir, + 'default_class': 'xmodule.hidden_module.HiddenDescriptor', + } + } +} TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT -TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) +# TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR) TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) +MY_TEST_DATA_XML_MODULESTORE = my_xml_store_config(TEST_DATA_DIR) REAL_DATA_DIR = settings.GITHUB_REPO_ROOT REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR) @@ -252,34 +258,93 @@ class PageLoader(ActivateLoginTestCase): #print descriptor.__class__, descriptor.location resp = self.client.get(reverse('jump_to', kwargs={'course_id': course_id, - 'location': descriptor.location.url()})) + 'location': descriptor.location.url()}), follow=True) msg = str(resp.status_code) - - if resp.status_code != 302: + if resp.status_code != 200: + msg = "ERROR " + msg # + ": " + str(resp.request['PATH_INFO']) + all_ok = False + num_bad += 1 + elif resp.redirect_chain[0][1] != 302: msg = "ERROR " + msg all_ok = False num_bad += 1 print msg - self.assertTrue(all_ok) # fail fast +# self.assertTrue(all_ok) # fail fast + + print "{0}/{1} good".format(n - num_bad, n) + self.assertTrue(all_ok) + + def check_xml_pages_load(self, course_name, data_dir, modstore): + """Make all locations in course load""" + print "Checking course {0} in {1}".format(course_name, data_dir) + default_class='xmodule.hidden_module.HiddenDescriptor' # 'xmodule.raw_module.RawDescriptor', + load_error_modules=True + module_store = XMLModuleStore( + data_dir, + default_class=default_class, + course_dirs=[course_name], + load_error_modules=load_error_modules, + ) +# for course_id in module_store.modules.keys(): +# for module in module_store.modules[course_id].itervalues(): +# +# if 'data' in module.definition: +# store.update_item(module.location, module.definition['data']) +# if 'children' in module.definition: +# store.update_children(module.location, module.definition['children']) +# # NOTE: It's important to use own_metadata here to avoid writing +# # inherited metadata everywhere. +# store.update_metadata(module.location, dict(module.own_metadata)) + # enroll in the course before trying to access pages + courses = module_store.get_courses() + self.assertEqual(len(courses), 1) + course = courses[0] + self.enroll(course) + course_id = course.id + + n = 0 + num_bad = 0 + all_ok = True + for descriptor in module_store.modules[course_id].itervalues(): + n += 1 + print "Checking ", descriptor.location.url() + #print descriptor.__class__, descriptor.location + resp = self.client.get(reverse('jump_to', + kwargs={'course_id': course_id, + 'location': descriptor.location.url()}), follow=True) + msg = str(resp.status_code) + if resp.status_code != 200: + msg = "ERROR " + msg # + ": " + str(resp.request['PATH_INFO']) + all_ok = False + num_bad += 1 + elif resp.redirect_chain[0][1] != 302: + msg = "ERROR " + msg + all_ok = False + num_bad += 1 + print msg +# self.assertTrue(all_ok) # fail fast print "{0}/{1} good".format(n - num_bad, n) self.assertTrue(all_ok) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +#@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@override_settings(MODULESTORE=MY_TEST_DATA_XML_MODULESTORE) class TestCoursesLoadTestCase(PageLoader): '''Check that all pages in test courses load properly''' def setUp(self): ActivateLoginTestCase.setUp(self) xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - +# xmodule.modulestore.django.modulestore().collection.drop() +# store = xmodule.modulestore.django.modulestore() + # is there a way to empty the store? + def test_toy_course_loads(self): - self.check_pages_load('toy', TEST_DATA_DIR, modulestore()) + self.check_xml_pages_load('toy', TEST_DATA_DIR, modulestore()) - def test_full_course_loads(self): - self.check_pages_load('full', TEST_DATA_DIR, modulestore()) +# def test_full_course_loads(self): +# self.check_pages_load('full', TEST_DATA_DIR, modulestore()) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 73d40b05c5..276af80ca9 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -293,7 +293,6 @@ def index(request, course_id, chapter=None, section=None, return result - @ensure_csrf_cookie def jump_to(request, course_id, location): ''' @@ -318,12 +317,18 @@ def jump_to(request, course_id, location): except NoPathToItem: raise Http404("This location is not in any class: {0}".format(location)) + # choose the appropriate view (and provide the necessary args) based on the + # args provided by the redirect. # Rely on index to do all error handling and access control. - return redirect('courseware_position', - course_id=course_id, - chapter=chapter, - section=section, - position=position) + if chapter is None: + return redirect('courseware', course_id=course_id) + elif section is None: + return redirect('courseware_chapter', course_id=course_id, chapter=chapter) + elif position is None: + return redirect('courseware_section', course_id=course_id, chapter=chapter, section=section) + else: + return redirect('courseware_position', course_id=course_id, chapter=chapter, section=section, position=position) + @ensure_csrf_cookie def course_info(request, course_id): """ From 3b326048f9087b34cb8b0c74618fba3cee464da4 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sat, 1 Dec 2012 12:16:10 -0500 Subject: [PATCH 120/736] Don't log errors to console if there is no console (like IE8 and below). --- lms/static/coffee/src/courseware.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index 096094ead9..0992043e79 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -18,5 +18,6 @@ class @Courseware histg = new Histogram id, $(this).data('histogram') catch error histg = error - console.log(error) + if console? + console.log(error) return histg From 63c1a8ea10b3c315cdbe16434afd19fc3450eb55 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 3 Dec 2012 12:43:43 -0500 Subject: [PATCH 121/736] whitespace --- common/lib/capa/capa/responsetypes.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 508845f773..bda1f5f1b1 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1844,32 +1844,35 @@ class OpenEndedResponse(LoncapaResponse): Configure OpenEndedResponse from XML. ''' xml = self.xml - # TODO: XML can override external resource (grader/queue) URL self.url = xml.get('url', None) self.queue_name = xml.get('queuename', self.DEFAULT_QUEUE) - #Look for tag named openendedparam that encapsulates all grader settings + # The openendedparam tag encapsulates all grader settings oeparam = self.xml.find('openendedparam') - prompt=self.xml.find('prompt') - rubric=self.xml.find('openendedrubric') - self._parse_openendedresponse_xml(oeparam,prompt,rubric) + prompt = self.xml.find('prompt') + rubric = self.xml.find('openendedrubric') + self._parse(oeparam,prompt,rubric) - def stringify_children(self,node,strip_tags=True): + def stringify_children(self, node, strip_tags=True): """ - Modify code from stringify_children in xmodule. Didn't import directly in order to avoid capa depending - on xmodule (seems to be avoided in code) + Modify code from stringify_children in xmodule. Didn't import directly + in order to avoid capa depending on xmodule (seems to be avoided in + code) """ parts=[node.text] [parts.append((etree.tostring(p, with_tail=True))) for p in node.getchildren()] node_string=' '.join(parts) - #Strip html tags from prompt. This may need to be removed in order to display prompt to instructors properly. + # Strip html tags from result. This may need to be removed in order to + # display prompt to instructors properly. + # TODO: what breaks if this is removed? The ML code can strip tags + # as part of its input filtering. if strip_tags: node_string=re.sub('<[^<]+?>', '', node_string) return node_string - def _parse_openendedresponse_xml(self,oeparam,prompt,rubric): + def _parse(self, oeparam, prompt, rubric): ''' Parse OpenEndedResponse XML: self.initial_display @@ -1879,8 +1882,8 @@ class OpenEndedResponse(LoncapaResponse): self.answer - What to display when show answer is clicked ''' # Note that OpenEndedResponse is agnostic to the specific contents of grader_payload - prompt_string=self.stringify_children(prompt) - rubric_string=self.stringify_children(rubric) + prompt_string = self.stringify_children(prompt) + rubric_string = self.stringify_children(rubric) grader_payload = oeparam.find('grader_payload') grader_payload = grader_payload.text if grader_payload is not None else '' From 22ce306f6443966a72f872708b119eae8e8d5c90 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 3 Dec 2012 12:49:58 -0500 Subject: [PATCH 122/736] whitespace and comments --- common/lib/capa/capa/responsetypes.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index bda1f5f1b1..98421ce0b3 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1851,7 +1851,7 @@ class OpenEndedResponse(LoncapaResponse): oeparam = self.xml.find('openendedparam') prompt = self.xml.find('prompt') rubric = self.xml.find('openendedrubric') - self._parse(oeparam,prompt,rubric) + self._parse(oeparam, prompt, rubric) def stringify_children(self, node, strip_tags=True): """ @@ -1890,7 +1890,7 @@ class OpenEndedResponse(LoncapaResponse): #Update grader payload with student id. If grader payload not json, error. try: - grader_payload=json.loads(grader_payload) + grader_payload = json.loads(grader_payload) # NOTE: self.system.location is valid because the capa_module # __init__ adds it (easiest way to get problem location into # response types) @@ -1900,7 +1900,7 @@ class OpenEndedResponse(LoncapaResponse): 'prompt' : prompt_string, 'rubric' : rubric_string, }) - grader_payload=json.dumps(grader_payload) + grader_payload = json.dumps(grader_payload) except Exception as err: log.error("Grader payload is not a json object!") @@ -1924,9 +1924,9 @@ class OpenEndedResponse(LoncapaResponse): top_score = oeparam.find('max_score') if top_score is not None: try: - self.max_score= int(top_score.text) + self.max_score = int(top_score.text) except: - self.top_score=1 + self.max_score = 1 else: self.max_score = 1 @@ -1992,7 +1992,8 @@ class OpenEndedResponse(LoncapaResponse): # 2) Frontend: correctness='incomplete' eventually trickles down # through inputtypes.textbox and .filesubmission to inform the # browser to poll the LMS - cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg) + cmap.set(self.answer_id, queuestate=queuestate, + correctness='incomplete', msg=msg) return cmap @@ -2001,7 +2002,7 @@ class OpenEndedResponse(LoncapaResponse): (valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg) if not valid_score_msg: oldcmap.set(self.answer_id, - msg='Invalid grader reply. Please contact the course staff.') + msg = 'Invalid grader reply. Please contact the course staff.') return oldcmap correctness = 'correct' if correct else 'incorrect' @@ -2132,11 +2133,11 @@ class OpenEndedResponse(LoncapaResponse): feedback = self._format_feedback(score_result) - score_ratio=int(score_result['score'])/self.max_score - correct=False - if score_ratio>=.66: - correct=True + # HACK: for now, just assume it's correct if you got more than 2/3. + # Also assumes that score_result['score'] is an integer. + score_ratio = int(score_result['score']) / self.max_score + correct = (score_ratio >= 0.66) log.debug(feedback) try: From a44e8887a87394a13e833e484b9ed3325b39d1fa Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 3 Dec 2012 13:22:50 -0500 Subject: [PATCH 123/736] add docs on staff grading tab type - also updated some out-of-date docs --- doc/xml-format.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/xml-format.md b/doc/xml-format.md index 46082b90ad..d7c5027a79 100644 --- a/doc/xml-format.md +++ b/doc/xml-format.md @@ -418,6 +418,10 @@ If you want to customize the courseware tabs displayed for your course, specify * "external_link". Parameters "name", "link". * "textbooks". No parameters--generates tab names from book titles. * "progress". Parameter "name". +* "static_tab". Parameters "name", 'url_slug'--will look for tab contents in + 'tabs/{course_url_name}/{tab url_slug}.html' +* "staff_grading". No parameters. If specified, displays the staff grading tab for instructors. + # Tips for content developers @@ -429,9 +433,7 @@ before the week 1 material to make it easy to find in the file. * Come up with a consistent pattern for url_names, so that it's easy to know where to look for any piece of content. It will also help to come up with a standard way of splitting your content files. As a point of departure, we suggest splitting chapters, sequences, html, and problems into separate files. -* A heads up: our content management system will allow you to develop content through a web browser, but will be backed by this same xml at first. Once that happens, every element will be in its own file to make access and updates faster. - -* Prefer the most "semantic" name for containers: e.g., use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml. +* Prefer the most "semantic" name for containers: e.g., use problemset rather than sequential for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml. # Other file locations (info and about) From 251f552e64fb5ead819d823b4690e10f47258691 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 3 Dec 2012 14:30:36 -0500 Subject: [PATCH 124/736] Don't try and send exceptions to newrelic with a busted logging interface --- lms/envs/logsettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/logsettings.py b/lms/envs/logsettings.py index 2b001b0517..8bd61a9e67 100644 --- a/lms/envs/logsettings.py +++ b/lms/envs/logsettings.py @@ -40,7 +40,7 @@ def get_logger_config(log_dir, logging_env=logging_env, hostname=hostname) handlers = ['console', 'local'] if debug else ['console', - 'syslogger-remote', 'local', 'newrelic'] + 'syslogger-remote', 'local'] logger_config = { 'version': 1, From 65c56edb5cabea51f65b121e3bcc0a28122ebe5b Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 3 Dec 2012 15:04:34 -0500 Subject: [PATCH 125/736] Better error logging when login into queue fails --- common/lib/capa/capa/xqueue_interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/xqueue_interface.py b/common/lib/capa/capa/xqueue_interface.py index f145cad23c..798867955b 100644 --- a/common/lib/capa/capa/xqueue_interface.py +++ b/common/lib/capa/capa/xqueue_interface.py @@ -81,7 +81,11 @@ class XQueueInterface(object): # Log in, then try again if error and (msg == 'login_required'): - self._login() + (error, content) = self._login() + if error != 0: + # when the login fails + log.debug("Failed to login to queue: %s", content) + return (error, content) if files_to_upload is not None: # Need to rewind file pointers for f in files_to_upload: From 1d44ebb10c842b02371dd92af02db671785ce0b0 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 3 Dec 2012 16:22:25 -0500 Subject: [PATCH 126/736] Add more non-ascii characters to full dataset, and make fixes in xmodule code. --- common/djangoapps/mitxmako/shortcuts.py | 7 ++++- common/djangoapps/mitxmako/template.py | 11 +++++-- common/lib/xmodule/xmodule/html_module.py | 6 ++-- common/test/data/full/custom_tags/book | 2 +- common/test/data/full/custom_tags/discuss | 2 +- common/test/data/full/custom_tags/slides | 2 +- .../data/full/problem/Circuit_Sandbox.xml | 4 +-- .../data/full/problem/choiceresponse_demo.xml | 15 ++++----- .../test/data/full/problem/codeinput_demo.xml | 3 +- .../Administrivia_and_Circuit_Elements.xml | 4 +-- .../test/data/full/vertical/vertical_89.xml | 2 +- common/test/data/full/video/welcome.xml | 2 +- lms/djangoapps/courseware/tests/tests.py | 31 ++++++++++++++++--- 13 files changed, 64 insertions(+), 27 deletions(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index 181d3befd5..6aee39906a 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -42,7 +42,12 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary.update(context) # fetch and render template template = middleware.lookup[namespace].get_template(template_name) - return template.render_unicode(**context_dictionary) +# return template.render_unicode(**context_dictionary) + + output = template.render_unicode(**context_dictionary) +# log.info(' render_to_string of "{0}" as "{1}r"'.format(type(output), output)) + return output +# return template.render(**context_dictionary) def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py index 2d6fc026ca..efeb282d04 100644 --- a/common/djangoapps/mitxmako/template.py +++ b/common/djangoapps/mitxmako/template.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +log = logging.getLogger("mitx." + __name__) + from django.conf import settings from mako.template import Template as MakoTemplate @@ -54,5 +57,9 @@ class Template(MakoTemplate): context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL context_dictionary['django_context'] = context_instance - return super(Template, self).render_unicode(**context_dictionary) - +# return super(Template, self).render_unicode(**context_dictionary) +# return super(Template, self).render(**context_dictionary) + + output = super(Template, self).render(**context_dictionary) + log.info(' render_to_string of "{0}" as "{1}"'.format(type(output), output)) + return output diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index eea747e332..6b73535dac 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -30,8 +30,10 @@ class HtmlModule(XModule): def get_html(self): # cdodge: perform link substitutions for any references to course static content (e.g. images) + input = self.html output = rewrite_links(self.html, self.rewrite_content_links) - log.info(' HTMLModule converting markup "{0}" to "{1}"'.format(self.html, output)) +# log.info(' HTMLModule converting markup from "{0}" as "{1}r"'.format(type(input), input)) +# log.info(' HTMLModule converting markup to "{0}" as "{1}r"'.format(type(output), output)) return output def __init__(self, system, location, definition, descriptor, @@ -166,7 +168,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(self.definition['data'].encode('utf-8')) + file.write(self.definition['data']) # .encode('utf-8')) # write out the relative name relname = path(pathname).basename() diff --git a/common/test/data/full/custom_tags/book b/common/test/data/full/custom_tags/book index ece6f288db..32e0913e6d 100644 --- a/common/test/data/full/custom_tags/book +++ b/common/test/data/full/custom_tags/book @@ -1 +1 @@ -More information given in the text. +More information given in… the text. diff --git a/common/test/data/full/custom_tags/discuss b/common/test/data/full/custom_tags/discuss index ac56590074..7a8a9e985f 100644 --- a/common/test/data/full/custom_tags/discuss +++ b/common/test/data/full/custom_tags/discuss @@ -1 +1 @@ - Discussion: ${tag} \ No newline at end of file + Discussion: ${tag}… \ No newline at end of file diff --git a/common/test/data/full/custom_tags/slides b/common/test/data/full/custom_tags/slides index a93d94947c..967c203711 100644 --- a/common/test/data/full/custom_tags/slides +++ b/common/test/data/full/custom_tags/slides @@ -1 +1 @@ -Lecture Slides Handout [Clean ][Annotated] +Lecture Slides Handout [Clean… ][Annotated…] diff --git a/common/test/data/full/problem/Circuit_Sandbox.xml b/common/test/data/full/problem/Circuit_Sandbox.xml index 89625f447b..1582f3ff0b 100644 --- a/common/test/data/full/problem/Circuit_Sandbox.xml +++ b/common/test/data/full/problem/Circuit_Sandbox.xml @@ -1,6 +1,6 @@ -

Here's a sandbox where you can experiment with all the components +

Here's a sandbox where you can experiment with all the components we'll discuss in 6.002x. If you click on CHECK below, your diagram -will be saved on the server and you can return at some later time. +will be saved on the server and you can return at some later time…

correct = ['correct']
diff --git a/common/test/data/full/problem/choiceresponse_demo.xml b/common/test/data/full/problem/choiceresponse_demo.xml index f7d1fcf16c..7af7939d74 100644 --- a/common/test/data/full/problem/choiceresponse_demo.xml +++ b/common/test/data/full/problem/choiceresponse_demo.xml @@ -1,19 +1,20 @@ -

Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to leftin the plane of your screen. A diagram of this situation is show below.

+

Consider a hypothetical magnetic field pointing out of your computer screen. Now imagine an electron traveling from right to left in the plane of your screen. A diagram of this situation is show below…

a. The magnitude of the force experienced by the electron is proportional the product of which of the following? (Select all that apply.)

-Magnetic field strength -Electric field strength -Electric charge of the electron -Radius of the electron -Mass of the electron -Velocity of the electron + +Magnetic field strength… +Electric field strength… +Electric charge of the electron… +Radius of the electron… +Mass of the electron… +Velocity of the electron… diff --git a/common/test/data/full/problem/codeinput_demo.xml b/common/test/data/full/problem/codeinput_demo.xml index 03d8fd8c31..a6662cb69c 100644 --- a/common/test/data/full/problem/codeinput_demo.xml +++ b/common/test/data/full/problem/codeinput_demo.xml @@ -2,7 +2,8 @@

- Part 1: Function Types + + Part 1: Function Types…

For each of the following functions, specify the type of its output. You can assume each function is called with an appropriate argument, as specified by its docstring.

diff --git a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml index 5c4c65f12d..d0239198af 100644 --- a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml +++ b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml @@ -3,12 +3,12 @@ - S1E4 has been removed. + S1E4 has been removed… diff --git a/common/test/data/full/vertical/vertical_89.xml b/common/test/data/full/vertical/vertical_89.xml index da15a6751a..a4716366fe 100644 --- a/common/test/data/full/vertical/vertical_89.xml +++ b/common/test/data/full/vertical/vertical_89.xml @@ -1,6 +1,6 @@ -

+

Inline content…

diff --git a/common/test/data/full/html/html_5555.html b/common/test/data/full/html/html_5555.html index 44a015faa1..25a0a40b75 100644 --- a/common/test/data/full/html/html_5555.html +++ b/common/test/data/full/html/html_5555.html @@ -1 +1 @@ - Lab Introduction or Interactive Lab Usage Handout for information on how to do the lab + Lab Introduction or Interactive Lab Usage Handout for information on how to do the lab. Gratuitous ≥ entity. From 9b1cad90b5ed56d7745ddefbc4885d8ddd34d0a7 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Mon, 3 Dec 2012 16:52:29 -0500 Subject: [PATCH 128/736] Fix location that gets used for open-ended responses --- common/lib/xmodule/xmodule/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index ead138a225..4c10a1703a 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -149,7 +149,7 @@ class CapaModule(XModule): # Need the problem location in openendedresponse to send out. Adding # it to the system here seems like the least clunky way to get it # there. - self.system.set('location', self.location) + self.system.set('location', self.location.url()) try: # TODO (vshnayder): move as much as possible of this work and error From 4ebd70dcc4ec40eaace413616dca13d3212f6d1b Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 4 Dec 2012 03:54:36 -0500 Subject: [PATCH 129/736] add support for utf8 in sequences, verticals and custom modules. --- common/djangoapps/mitxmako/shortcuts.py | 7 +------ common/djangoapps/mitxmako/template.py | 7 +------ common/lib/xmodule/xmodule/error_module.py | 4 ++-- common/lib/xmodule/xmodule/html_module.py | 6 +----- common/lib/xmodule/xmodule/modulestore/xml.py | 4 ++-- common/lib/xmodule/xmodule/raw_module.py | 2 +- common/lib/xmodule/xmodule/seq_module.py | 2 +- common/lib/xmodule/xmodule/stringify.py | 4 ++-- common/lib/xmodule/xmodule/xml_module.py | 8 +++++--- common/test/data/full/chapter/Overview.xml | 2 +- common/test/data/full/html/FirstOrderSystemHint.xml | 2 +- common/test/data/full/html/Midterm_Exam_1123.html | 2 +- common/test/data/full/html/Week_13_Tutorials.html | 4 ++-- common/test/data/full/html/html_5555.html | 2 +- common/test/data/full/html/linearity_clarify.html | 2 +- common/test/data/full/html/linearity_clarify.xml | 2 +- common/test/data/full/html/schematic_tutorial.html | 6 +++--- common/test/data/full/html/units_hint.html | 4 ++-- common/test/data/full/info/updates.html | 5 +++-- .../sequential/Administrivia_and_Circuit_Elements.xml | 3 ++- common/test/data/full/vertical/vertical_89.xml | 1 + 21 files changed, 35 insertions(+), 44 deletions(-) diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py index 6aee39906a..181d3befd5 100644 --- a/common/djangoapps/mitxmako/shortcuts.py +++ b/common/djangoapps/mitxmako/shortcuts.py @@ -42,12 +42,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary.update(context) # fetch and render template template = middleware.lookup[namespace].get_template(template_name) -# return template.render_unicode(**context_dictionary) - - output = template.render_unicode(**context_dictionary) -# log.info(' render_to_string of "{0}" as "{1}r"'.format(type(output), output)) - return output -# return template.render(**context_dictionary) + return template.render_unicode(**context_dictionary) def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs): diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py index efeb282d04..ff83a9be4c 100644 --- a/common/djangoapps/mitxmako/template.py +++ b/common/djangoapps/mitxmako/template.py @@ -57,9 +57,4 @@ class Template(MakoTemplate): context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL context_dictionary['django_context'] = context_instance -# return super(Template, self).render_unicode(**context_dictionary) -# return super(Template, self).render(**context_dictionary) - - output = super(Template, self).render(**context_dictionary) - log.info(' render_to_string of "{0}" as "{1}"'.format(type(output), output)) - return output + return super(Template, self).render_unicode(**context_dictionary) diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index 65fceb77c7..2df47e05e6 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -149,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor): ''' try: xml = etree.fromstring(self.definition['data']['contents']) - return etree.tostring(xml) + return etree.tostring(xml, encoding='unicode') except etree.XMLSyntaxError: # still not valid. root = etree.Element('error') root.text = self.definition['data']['contents'] err_node = etree.SubElement(root, 'error_msg') err_node.text = self.definition['data']['error_msg'] - return etree.tostring(root) + return etree.tostring(root, encoding='unicode') class NonStaffErrorDescriptor(ErrorDescriptor): diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 6b73535dac..df77f43a17 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -30,11 +30,7 @@ class HtmlModule(XModule): def get_html(self): # cdodge: perform link substitutions for any references to course static content (e.g. images) - input = self.html - output = rewrite_links(self.html, self.rewrite_content_links) -# log.info(' HTMLModule converting markup from "{0}" as "{1}r"'.format(type(input), input)) -# log.info(' HTMLModule converting markup to "{0}" as "{1}r"'.format(type(output), output)) - return output + return rewrite_links(self.html, self.rewrite_content_links) def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 6f8430917d..6b3ff9bff4 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -152,7 +152,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): make_name_unique(xml_data) descriptor = XModuleDescriptor.load_from_xml( - etree.tostring(xml_data), self, self.org, + etree.tostring(xml_data, encoding='unicode'), self, self.org, self.course, xmlstore.default_class) except Exception as err: print err, self.load_error_modules @@ -436,7 +436,7 @@ class XMLModuleStore(ModuleStoreBase): self.load_error_modules, ) - course_descriptor = system.process_xml(etree.tostring(course_data)) + course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode')) # NOTE: The descriptors end up loading somewhat bottom up, which # breaks metadata inheritance via get_children(). Instead diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py index 5ff16098ac..efdd2e7ba0 100644 --- a/common/lib/xmodule/xmodule/raw_module.py +++ b/common/lib/xmodule/xmodule/raw_module.py @@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor): """ @classmethod def definition_from_xml(cls, xml_object, system): - return {'data': etree.tostring(xml_object, pretty_print=True)} + return {'data': etree.tostring(xml_object, pretty_print=True,encoding='unicode')} def definition_to_xml(self, resource_fs): try: diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index b625646e66..9aec275e90 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -124,7 +124,7 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): children = [] for child in xml_object: try: - children.append(system.process_xml(etree.tostring(child)).location.url()) + children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url()) except: log.exception("Unable to load child when parsing Sequence. Continuing...") continue diff --git a/common/lib/xmodule/xmodule/stringify.py b/common/lib/xmodule/xmodule/stringify.py index 1e3fa91210..dab8ff0425 100644 --- a/common/lib/xmodule/xmodule/stringify.py +++ b/common/lib/xmodule/xmodule/stringify.py @@ -22,7 +22,7 @@ def stringify_children(node): # next element. parts = [node.text] for c in node.getchildren(): - parts.append(etree.tostring(c, with_tail=True)) + parts.append(etree.tostring(c, with_tail=True, encoding='unicode')) # filter removes possible Nones in texts and tails - return ''.join(filter(None, parts)) + return u''.join(filter(None, parts)) diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index e65a8c74ea..91768d8e11 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -13,7 +13,8 @@ import sys log = logging.getLogger(__name__) edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True, remove_blank_text=True) + remove_comments=True, remove_blank_text=True, + encoding='utf-8') def name_to_pathname(name): """ @@ -206,6 +207,7 @@ class XmlDescriptor(XModuleDescriptor): definition_xml = cls.load_file(filepath, system.resources_fs, location) + log.info(' read definition XML: %s', definition_xml) definition_metadata = get_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml) definition = cls.definition_from_xml(definition_xml, system) @@ -366,7 +368,7 @@ class XmlDescriptor(XModuleDescriptor): filepath = self.__class__._format_filepath(self.category, url_path) resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) with resource_fs.open(filepath, 'w') as file: - file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8', xml_declaration=True)) + file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8')) # And return just a pointer with the category and filename. record_object = etree.Element(self.category) @@ -381,7 +383,7 @@ class XmlDescriptor(XModuleDescriptor): record_object.set('org', self.location.org) record_object.set('course', self.location.course) - return etree.tostring(record_object, pretty_print=True) + return etree.tostring(record_object, pretty_print=True, encoding='utf-8') def definition_to_xml(self, resource_fs): """ diff --git a/common/test/data/full/chapter/Overview.xml b/common/test/data/full/chapter/Overview.xml index a11a11a1e0..8ad44b366c 100644 --- a/common/test/data/full/chapter/Overview.xml +++ b/common/test/data/full/chapter/Overview.xml @@ -2,7 +2,7 @@