Commit 120aa3cd by Diana Huang

Merge pull request #1421 from MITx/feature/vik/peer-grading-xmodule

Feature/vik/peer grading xmodule
parents 74c112e5 ce87e6f2
...@@ -64,7 +64,6 @@ def replace_static_urls(get_html, data_dir, course_namespace=None): ...@@ -64,7 +64,6 @@ def replace_static_urls(get_html, data_dir, course_namespace=None):
return static_replace.replace_static_urls(get_html(), data_dir, course_namespace) return static_replace.replace_static_urls(get_html(), data_dir, course_namespace)
return _get_html return _get_html
def grade_histogram(module_id): def grade_histogram(module_id):
''' Print out a histogram of grades on a given problem. ''' Print out a histogram of grades on a given problem.
Part of staff member debug info. Part of staff member debug info.
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var MinimaxProblemDisplay, root, var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var TestProblemGenerator, root, var TestProblemGenerator, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var TestProblemGrader, root, var TestProblemGrader, root,
__hasProp = {}.hasOwnProperty, __hasProp = {}.hasOwnProperty,
......
// Generated by CoffeeScript 1.3.3 // Generated by CoffeeScript 1.4.0
(function() { (function() {
var XProblemDisplay, XProblemGenerator, XProblemGrader, root; var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
......
...@@ -27,6 +27,7 @@ setup( ...@@ -27,6 +27,7 @@ setup(
"html = xmodule.html_module:HtmlDescriptor", "html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"problem = xmodule.capa_module:CapaDescriptor", "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor", "randomize = xmodule.randomize_module:RandomizeDescriptor",
......
...@@ -33,7 +33,9 @@ class CombinedOpenEndedRubric(object): ...@@ -33,7 +33,9 @@ class CombinedOpenEndedRubric(object):
'view_only': self.view_only}) 'view_only': self.view_only})
success = True success = True
except: except:
raise RubricParsingError("[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)) error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)
log.error(error_message)
raise RubricParsingError(error_message)
return success, html return success, html
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
......
...@@ -5,16 +5,8 @@ import requests ...@@ -5,16 +5,8 @@ import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from django.conf import settings
from django.http import HttpResponse, Http404
from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from lxml import etree from lxml import etree
from mitxmako.shortcuts import render_to_string
from xmodule.x_module import ModuleSystem
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -31,7 +23,7 @@ class GradingService(object): ...@@ -31,7 +23,7 @@ class GradingService(object):
self.url = config['url'] self.url = config['url']
self.login_url = self.url + '/login/' self.login_url = self.url + '/login/'
self.session = requests.session() self.session = requests.session()
self.system = ModuleSystem(None, None, None, render_to_string, None) self.system = config['system']
def _login(self): def _login(self):
""" """
...@@ -42,20 +34,20 @@ class GradingService(object): ...@@ -42,20 +34,20 @@ class GradingService(object):
Returns the decoded json dict of the response. Returns the decoded json dict of the response.
""" """
response = self.session.post(self.login_url, response = self.session.post(self.login_url,
{'username': self.username, {'username': self.username,
'password': self.password,}) 'password': self.password,})
response.raise_for_status() response.raise_for_status()
return response.json return response.json
def post(self, url, data, allow_redirects=False): def post(self, url, data, allow_redirects=False):
""" """
Make a post request to the grading controller Make a post request to the grading controller
""" """
try: try:
op = lambda: self.session.post(url, data=data, op = lambda: self.session.post(url, data=data,
allow_redirects=allow_redirects) allow_redirects=allow_redirects)
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace. # reraise as promised GradingServiceError, but preserve stacktrace.
...@@ -69,8 +61,8 @@ class GradingService(object): ...@@ -69,8 +61,8 @@ class GradingService(object):
""" """
log.debug(params) log.debug(params)
op = lambda: self.session.get(url, op = lambda: self.session.get(url,
allow_redirects=allow_redirects, allow_redirects=allow_redirects,
params=params) params=params)
try: try:
r = self._try_with_login(op) r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err: except (RequestException, ConnectionError, HTTPError) as err:
...@@ -78,7 +70,7 @@ class GradingService(object): ...@@ -78,7 +70,7 @@ class GradingService(object):
raise GradingServiceError, str(err), sys.exc_info()[2] raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text return r.text
def _try_with_login(self, operation): def _try_with_login(self, operation):
""" """
...@@ -96,8 +88,8 @@ class GradingService(object): ...@@ -96,8 +88,8 @@ class GradingService(object):
r = self._login() r = self._login()
if r and not r.get('success'): if r and not r.get('success'):
log.warning("Couldn't log into staff_grading backend. Response: %s", log.warning("Couldn't log into staff_grading backend. Response: %s",
r) r)
# try again # try again
response = operation() response = operation()
response.raise_for_status() response.raise_for_status()
...@@ -113,23 +105,23 @@ class GradingService(object): ...@@ -113,23 +105,23 @@ class GradingService(object):
""" """
try: try:
response_json = json.loads(response) response_json = json.loads(response)
except:
response_json = response
try:
if 'rubric' in response_json: if 'rubric' in response_json:
rubric = response_json['rubric'] rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(self.system, False) rubric_renderer = CombinedOpenEndedRubric(self.system, False)
success, rubric_html = rubric_renderer.render_rubric(rubric) success, rubric_html = rubric_renderer.render_rubric(rubric)
response_json['rubric'] = rubric_html response_json['rubric'] = rubric_html
return response_json return response_json
# if we can't parse the rubric into HTML, # if we can't parse the rubric into HTML,
except etree.XMLSyntaxError, RubricParsingError: except etree.XMLSyntaxError, RubricParsingError:
log.exception("Cannot parse rubric string. Raw string: {0}" log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric)) .format(rubric))
return {'success': False, return {'success': False,
'error': 'Error displaying submission'} 'error': 'Error displaying submission'}
except ValueError: except ValueError:
log.exception("Error parsing response: {0}".format(response)) log.exception("Error parsing response: {0}".format(response))
return {'success': False, return {'success': False,
'error': "Error displaying submission"} 'error': "Error displaying submission"}
\ No newline at end of file
...@@ -2,17 +2,27 @@ ...@@ -2,17 +2,27 @@
# and message container when they are empty # and message container when they are empty
# Can (and should be) expanded upon when our problem list # Can (and should be) expanded upon when our problem list
# becomes more sophisticated # becomes more sophisticated
class PeerGrading class @PeerGrading
constructor: () -> constructor: (element) ->
@peer_grading_container = $('.peer-grading')
@use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container')
@ajax_url = @peer_grading_container.data('ajax-url')
@error_container = $('.error-container') @error_container = $('.error-container')
@error_container.toggle(not @error_container.is(':empty')) @error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container') @message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty')) @message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button.click @show_results
@problem_list = $('.problem-list') @problem_list = $('.problem-list')
@construct_progress_bar() @construct_progress_bar()
if @use_single_location
@activate_problem()
construct_progress_bar: () => construct_progress_bar: () =>
problems = @problem_list.find('tr').next() problems = @problem_list.find('tr').next()
problems.each( (index, element) => problems.each( (index, element) =>
...@@ -22,6 +32,18 @@ class PeerGrading ...@@ -22,6 +32,18 @@ class PeerGrading
bar_max = parseInt(problem.data('required')) + bar_value bar_max = parseInt(problem.data('required')) + bar_value
progress_bar.progressbar({value: bar_value, max: bar_max}) progress_bar.progressbar({value: bar_value, max: bar_max})
) )
$(document).ready(() -> new PeerGrading()) show_results: (event) =>
location_to_fetch = $(event.target).data('location')
data = {'location' : location_to_fetch}
$.postWithPrefix "#{@ajax_url}problem", data, (response) =>
if response.success
@peer_grading_outer_container.after(response.html).remove()
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
else
@gentle_alert response.error
activate_problem: () =>
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
\ No newline at end of file
...@@ -13,6 +13,10 @@ from urlparse import urlparse ...@@ -13,6 +13,10 @@ from urlparse import urlparse
import requests import requests
from boto.s3.connection import S3Connection from boto.s3.connection import S3Connection
from boto.s3.key import Key from boto.s3.key import Key
#TODO: Settings import is needed now in order to specify the URL and keys for amazon s3 (to upload images).
#Eventually, the goal is to replace the global django settings import with settings specifically
#for this module. There is no easy way to do this now, so piggybacking on the django settings
#makes sense.
from django.conf import settings from django.conf import settings
import pickle import pickle
import logging import logging
......
import json
import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys
#TODO: Settings import is needed now in order to specify the URL where to find the peer grading service.
#Eventually, the goal is to replace the global django settings import with settings specifically
#for this xmodule. There is no easy way to do this now, so piggybacking on the django settings
#makes sense.
from django.conf import settings
from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from lxml import etree
from grading_service_module import GradingService, GradingServiceError
log=logging.getLogger(__name__)
class GradingServiceError(Exception):
pass
class PeerGradingService(GradingService):
"""
Interface with the grading controller for peer grading
"""
def __init__(self, config, system):
config['system'] = system
super(PeerGradingService, self).__init__(config)
self.get_next_submission_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/'
self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
self.get_problem_list_url = self.url + '/get_problem_list/'
self.get_notifications_url = self.url + '/get_notifications/'
self.get_data_for_location_url = self.url + '/get_data_for_location/'
self.system = system
def get_data_for_location(self, problem_location, student_id):
response = self.get(self.get_data_for_location_url,
{'location': problem_location, 'student_id': student_id})
return self.try_to_decode(response)
def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id})
return self.try_to_decode(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores, submission_flagged):
data = {'grader_id' : grader_id,
'submission_id' : submission_id,
'score' : score,
'feedback' : feedback,
'submission_key': submission_key,
'location': location,
'rubric_scores': rubric_scores,
'rubric_scores_complete': True,
'submission_flagged' : submission_flagged}
return self.try_to_decode(self.post(self.save_grade_url, data))
def is_student_calibrated(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.try_to_decode(self.get(self.is_student_calibrated_url, params))
def show_calibration_essay(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id}
response = self.get(self.show_calibration_essay_url, params)
return self.try_to_decode(self._render_rubric(response))
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key,
score, feedback, rubric_scores):
data = {'location': problem_location,
'student_id': grader_id,
'calibration_essay_id': calibration_essay_id,
'submission_key': submission_key,
'score': score,
'feedback': feedback,
'rubric_scores[]': rubric_scores,
'rubric_scores_complete': True}
return self.try_to_decode(self.post(self.save_calibration_essay_url, data))
def get_problem_list(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_problem_list_url, params)
return self.try_to_decode(response)
def get_notifications(self, course_id, grader_id):
params = {'course_id': course_id, 'student_id': grader_id}
response = self.get(self.get_notifications_url, params)
return self.try_to_decode(response)
def try_to_decode(self, text):
try:
text = json.loads(text)
except:
pass
return text
"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
"""
class MockPeerGradingService(object):
def get_next_submission(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': "",
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_grade(self, location, grader_id, submission_id,
score, feedback, submission_key):
return json.dumps({'success': True})
def is_student_calibrated(self, problem_location, grader_id):
return json.dumps({'success': True, 'calibrated': True})
def show_calibration_essay(self, problem_location, grader_id):
return json.dumps({'success': True,
'submission_id':1,
'submission_key': '',
'student_response': 'fake student response',
'prompt': 'fake submission prompt',
'rubric': 'fake rubric',
'max_score': 4})
def save_calibration_essay(self, problem_location, grader_id,
calibration_essay_id, submission_key, score, feedback):
return {'success': True, 'actual_score': 2}
def get_problem_list(self, course_id, grader_id):
return json.dumps({'success': True,
'problem_list': [
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
]})
_service = None
def peer_grading_service(system):
"""
Return a peer grading service instance--if settings.MOCK_PEER_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_PEER_GRADING:
_service = MockPeerGradingService()
else:
_service = PeerGradingService(settings.PEER_GRADING_INTERFACE, system)
return _service
<peergrading display_name = "Peer Grading" use_for_single_location="False" is_graded="False"/>
...@@ -3,11 +3,12 @@ import logging ...@@ -3,11 +3,12 @@ import logging
import requests import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from grading_service import GradingService from xmodule.grading_service_module import GradingService, GradingServiceError
from grading_service import GradingServiceError
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -16,6 +17,7 @@ class ControllerQueryService(GradingService): ...@@ -16,6 +17,7 @@ class ControllerQueryService(GradingService):
Interface to staff grading backend. Interface to staff grading backend.
""" """
def __init__(self, config): def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(ControllerQueryService, self).__init__(config) super(ControllerQueryService, self).__init__(config)
self.check_eta_url = self.url + '/get_submission_eta/' self.check_eta_url = self.url + '/get_submission_eta/'
self.is_unique_url = self.url + '/is_name_unique/' self.is_unique_url = self.url + '/is_name_unique/'
......
from django.conf import settings from django.conf import settings
from staff_grading_service import StaffGradingService from staff_grading_service import StaffGradingService
from peer_grading_service import PeerGradingService
from open_ended_grading.controller_query_service import ControllerQueryService from open_ended_grading.controller_query_service import ControllerQueryService
import json import json
from student.models import unique_id_for_user from student.models import unique_id_for_user
...@@ -10,6 +9,7 @@ import logging ...@@ -10,6 +9,7 @@ import logging
from courseware.access import has_access from courseware.access import has_access
from util.cache import cache from util.cache import cache
import datetime import datetime
from xmodule import peer_grading_service
log=logging.getLogger(__name__) log=logging.getLogger(__name__)
......
...@@ -7,8 +7,7 @@ import logging ...@@ -7,8 +7,7 @@ import logging
import requests import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys import sys
from grading_service import GradingService from xmodule.grading_service_module import GradingService, GradingServiceError
from grading_service import GradingServiceError
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
...@@ -22,8 +21,6 @@ from mitxmako.shortcuts import render_to_string ...@@ -22,8 +21,6 @@ from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MockStaffGradingService(object): class MockStaffGradingService(object):
""" """
A simple mockup of a staff grading service, testing. A simple mockup of a staff grading service, testing.
...@@ -64,6 +61,7 @@ class StaffGradingService(GradingService): ...@@ -64,6 +61,7 @@ class StaffGradingService(GradingService):
Interface to staff grading backend. Interface to staff grading backend.
""" """
def __init__(self, config): def __init__(self, config):
config['system'] = ModuleSystem(None,None,None,render_to_string,None)
super(StaffGradingService, self).__init__(config) super(StaffGradingService, self).__init__(config)
self.get_next_url = self.url + '/get_next_submission/' self.get_next_url = self.url + '/get_next_submission/'
self.save_grade_url = self.url + '/save_grade/' self.save_grade_url = self.url + '/save_grade/'
......
...@@ -6,7 +6,7 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open ...@@ -6,7 +6,7 @@ django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/open
from django.test import TestCase from django.test import TestCase
from open_ended_grading import staff_grading_service from open_ended_grading import staff_grading_service
from open_ended_grading import peer_grading_service from xmodule import peer_grading_service, peer_grading_module
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
...@@ -17,10 +17,13 @@ import xmodule.modulestore.django ...@@ -17,10 +17,13 @@ import xmodule.modulestore.django
from nose import SkipTest from nose import SkipTest
from mock import patch, Mock from mock import patch, Mock
import json import json
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from override_settings import override_settings from override_settings import override_settings
from django.http import QueryDict
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
...@@ -98,6 +101,7 @@ class TestStaffGradingService(ct.PageLoader): ...@@ -98,6 +101,7 @@ class TestStaffGradingService(ct.PageLoader):
'submission_id': '123', 'submission_id': '123',
'location': self.location, 'location': self.location,
'rubric_scores[]': ['1', '2']} 'rubric_scores[]': ['1', '2']}
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(d['success'], str(d))
...@@ -136,19 +140,21 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -136,19 +140,21 @@ class TestPeerGradingService(ct.PageLoader):
self.course_id = "edX/toy/2012_Fall" self.course_id = "edX/toy/2012_Fall"
self.toy = modulestore().get_course(self.course_id) self.toy = modulestore().get_course(self.course_id)
location = "i4x://edX/toy/peergrading/init"
self.mock_service = peer_grading_service.peer_grading_service() self.mock_service = peer_grading_service.MockPeerGradingService()
self.system = ModuleSystem(location, None, None, render_to_string, None)
self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system)
self.peer_module = peer_grading_module.PeerGradingModule(self.system,location,"<peergrading/>",self.descriptor)
self.peer_module.peer_gs = self.mock_service
self.logout() self.logout()
def test_get_next_submission_success(self): def test_get_next_submission_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_get_next_submission', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.get_next_submission(data)
d = json.loads(r.content) d = json.loads(r)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(d['prompt'])
...@@ -156,63 +162,48 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -156,63 +162,48 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(d['max_score'])
def test_get_next_submission_missing_location(self): def test_get_next_submission_missing_location(self):
self.login(self.student, self.password)
url = reverse('peer_grading_get_next_submission', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.get_next_submission(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location") self.assertEqual(d['error'], "Missing required keys: location")
def test_save_grade_success(self): def test_save_grade_success(self):
self.login(self.student, self.password) raise SkipTest()
url = reverse('peer_grading_save_grade', kwargs={'course_id': self.course_id}) data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
data = {'location': self.location, qdict = QueryDict(data.replace("|","&"))
'submission_id': '1', r = self.peer_module.save_grade(qdict)
'submission_key': 'fake key', d = r
'score': '2',
'feedback': 'This is feedback',
'rubric_scores[]': [1, 2],
'submission_flagged' : False}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success']) self.assertTrue(d['success'])
def test_save_grade_missing_keys(self): def test_save_grade_missing_keys(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_grade', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.save_grade(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1) self.assertTrue(d['error'].find('Missing required keys:') > -1)
def test_is_calibrated_success(self): def test_is_calibrated_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_is_student_calibrated', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.is_student_calibrated(data)
d = json.loads(r.content) d = json.loads(r)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertTrue('calibrated' in d) self.assertTrue('calibrated' in d)
def test_is_calibrated_failure(self): def test_is_calibrated_failure(self):
self.login(self.student, self.password)
url = reverse('peer_grading_is_student_calibrated', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.is_student_calibrated(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertFalse('calibrated' in d) self.assertFalse('calibrated' in d)
def test_show_calibration_essay_success(self): def test_show_calibration_essay_success(self):
self.login(self.student, self.password)
url = reverse('peer_grading_show_calibration_essay', kwargs={'course_id': self.course_id})
data = {'location': self.location} data = {'location': self.location}
r = self.check_for_post_code(200, url, data) r = self.peer_module.show_calibration_essay(data)
d = json.loads(r.content) d = json.loads(r)
log.debug(d)
log.debug(type(d))
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertIsNotNone(d['submission_id']) self.assertIsNotNone(d['submission_id'])
self.assertIsNotNone(d['prompt']) self.assertIsNotNone(d['prompt'])
...@@ -220,37 +211,27 @@ class TestPeerGradingService(ct.PageLoader): ...@@ -220,37 +211,27 @@ class TestPeerGradingService(ct.PageLoader):
self.assertIsNotNone(d['max_score']) self.assertIsNotNone(d['max_score'])
def test_show_calibration_essay_missing_key(self): def test_show_calibration_essay_missing_key(self):
self.login(self.student, self.password)
url = reverse('peer_grading_show_calibration_essay', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.show_calibration_essay(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertEqual(d['error'], "Missing required keys: location") self.assertEqual(d['error'], "Missing required keys: location")
def test_save_calibration_essay_success(self): def test_save_calibration_essay_success(self):
self.login(self.student, self.password) raise SkipTest()
url = reverse('peer_grading_save_calibration_essay', kwargs={'course_id': self.course_id}) data = 'rubric_scores[]=1|rubric_scores[]=2|location=' + self.location + '|submission_id=1|submission_key=fake key|score=2|feedback=feedback|submission_flagged=False'
data = {'location': self.location, qdict = QueryDict(data.replace("|","&"))
'submission_id': '1', r = self.peer_module.save_calibration_essay(qdict)
'submission_key': 'fake key', d = r
'score': '2',
'feedback': 'This is feedback',
'rubric_scores[]': [1, 2]}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success']) self.assertTrue(d['success'])
self.assertTrue('actual_score' in d) self.assertTrue('actual_score' in d)
def test_save_calibration_essay_missing_keys(self): def test_save_calibration_essay_missing_keys(self):
self.login(self.student, self.password)
url = reverse('peer_grading_save_calibration_essay', kwargs={'course_id': self.course_id})
data = {} data = {}
r = self.check_for_post_code(200, url, data) r = self.peer_module.save_calibration_essay(data)
d = json.loads(r.content) d = r
self.assertFalse(d['success']) self.assertFalse(d['success'])
self.assertTrue(d['error'].find('Missing required keys:') > -1) self.assertTrue(d['error'].find('Missing required keys:') > -1)
self.assertFalse('actual_score' in d) self.assertFalse('actual_score' in d)
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import logging import logging
import urllib import urllib
import re
from django.conf import settings from django.conf import settings
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
...@@ -11,10 +12,8 @@ from django.core.urlresolvers import reverse ...@@ -11,10 +12,8 @@ from django.core.urlresolvers import reverse
from student.models import unique_id_for_user from student.models import unique_id_for_user
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from peer_grading_service import PeerGradingService
from peer_grading_service import MockPeerGradingService
from controller_query_service import ControllerQueryService from controller_query_service import ControllerQueryService
from grading_service import GradingServiceError from xmodule.grading_service_module import GradingServiceError
import json import json
from .staff_grading import StaffGrading from .staff_grading import StaffGrading
from student.models import unique_id_for_user from student.models import unique_id_for_user
...@@ -25,15 +24,11 @@ import open_ended_notifications ...@@ -25,15 +24,11 @@ import open_ended_notifications
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import search from xmodule.modulestore import search
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404, HttpResponseRedirect
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
if settings.MOCK_PEER_GRADING:
peer_gs = MockPeerGradingService()
else:
peer_gs = PeerGradingService(settings.PEER_GRADING_INTERFACE)
controller_url = open_ended_util.get_controller_url() controller_url = open_ended_util.get_controller_url()
controller_qs = ControllerQueryService(controller_url) controller_qs = ControllerQueryService(controller_url)
...@@ -81,66 +76,44 @@ def staff_grading(request, course_id): ...@@ -81,66 +76,44 @@ def staff_grading(request, course_id):
# Checked above # Checked above
'staff_access': True, }) 'staff_access': True, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def peer_grading(request, course_id): def peer_grading(request, course_id):
''' '''
Show a peer grading interface Show a peer grading interface
''' '''
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
course_id_parts = course.id.split("/")
course_id_norun = "/".join(course_id_parts[0:2])
pg_location = "i4x://" + course_id_norun + "/peergrading/init"
# call problem list service base_course_url = reverse('courses')
success = False
error_text = ""
problem_list = []
try: try:
problem_list_json = peer_gs.get_problem_list(course_id, unique_id_for_user(request.user)) problem_url_parts = search.path_to_location(modulestore(), course.id, pg_location)
problem_list_dict = json.loads(problem_list_json) problem_url = generate_problem_url(problem_url_parts, base_course_url)
success = problem_list_dict['success']
if 'error' in problem_list_dict:
error_text = problem_list_dict['error']
problem_list = problem_list_dict['problem_list']
except GradingServiceError:
error_text = "Error occured while contacting the grading service"
success = False
# catch error if if the json loads fails
except ValueError:
error_text = "Could not get problem list"
success = False
ajax_url = _reverse_with_slash('peer_grading', course_id)
return render_to_response('peer_grading/peer_grading.html', { return HttpResponseRedirect(problem_url)
'course': course, except:
'course_id': course_id, error_message = "Error with initializing peer grading. Centralized module does not exist. Please contact course staff."
'ajax_url': ajax_url, log.exception(error_message + "Current course is: {0}".format(course_id))
'success': success, return HttpResponse(error_message)
'problem_list': problem_list,
'error_text': error_text,
# Checked above
'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) def generate_problem_url(problem_url_parts, base_course_url):
def peer_grading_problem(request, course_id): """
''' From a list of problem url parts generated by search.path_to_location and a base course url, generates a url to a problem
Show individual problem interface @param problem_url_parts: Output of search.path_to_location
''' @param base_course_url: Base url of a given course
course = get_course_with_access(request.user, course_id, 'load') @return: A path to the problem
problem_location = request.GET.get("location") """
problem_url = base_course_url + "/"
ajax_url = _reverse_with_slash('peer_grading', course_id) for z in xrange(0,len(problem_url_parts)):
part = problem_url_parts[z]
if part is not None:
if z==1:
problem_url += "courseware/"
problem_url += part + "/"
return problem_url
return render_to_response('peer_grading/peer_grading_problem.html', {
'view_html': '',
'course': course,
'problem_location': problem_location,
'course_id': course_id,
'ajax_url': ajax_url,
# Checked above
'staff_access': False, })
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def student_problem_list(request, course_id): def student_problem_list(request, course_id):
...@@ -156,28 +129,22 @@ def student_problem_list(request, course_id): ...@@ -156,28 +129,22 @@ def student_problem_list(request, course_id):
problem_list = [] problem_list = []
base_course_url = reverse('courses') base_course_url = reverse('courses')
try: #try:
problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user)) problem_list_json = controller_qs.get_grading_status_list(course_id, unique_id_for_user(request.user))
problem_list_dict = json.loads(problem_list_json) problem_list_dict = json.loads(problem_list_json)
success = problem_list_dict['success'] success = problem_list_dict['success']
if 'error' in problem_list_dict: if 'error' in problem_list_dict:
error_text = problem_list_dict['error'] error_text = problem_list_dict['error']
problem_list = [] problem_list = []
else: else:
problem_list = problem_list_dict['problem_list'] problem_list = problem_list_dict['problem_list']
for i in xrange(0,len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
problem_url = base_course_url + "/"
for z in xrange(0,len(problem_url_parts)):
part = problem_url_parts[z]
if part is not None:
if z==1:
problem_url += "courseware/"
problem_url += part + "/"
problem_list[i].update({'actual_url' : problem_url}) for i in xrange(0,len(problem_list)):
problem_url_parts = search.path_to_location(modulestore(), course.id, problem_list[i]['location'])
problem_url = generate_problem_url(problem_url_parts, base_course_url)
problem_list[i].update({'actual_url' : problem_url})
"""
except GradingServiceError: except GradingServiceError:
error_text = "Error occured while contacting the grading service" error_text = "Error occured while contacting the grading service"
success = False success = False
...@@ -185,6 +152,7 @@ def student_problem_list(request, course_id): ...@@ -185,6 +152,7 @@ def student_problem_list(request, course_id):
except ValueError: except ValueError:
error_text = "Could not get problem list" error_text = "Could not get problem list"
success = False success = False
"""
ajax_url = _reverse_with_slash('open_ended_problems', course_id) ajax_url = _reverse_with_slash('open_ended_problems', course_id)
...@@ -231,16 +199,17 @@ def flagged_problem_list(request, course_id): ...@@ -231,16 +199,17 @@ def flagged_problem_list(request, course_id):
success = False success = False
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id) ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_id)
context = {
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', { 'course': course,
'course': course, 'course_id': course_id,
'course_id': course_id, 'ajax_url': ajax_url,
'ajax_url': ajax_url, 'success': success,
'success': success, 'problem_list': problem_list,
'problem_list': problem_list, 'error_text': error_text,
'error_text': error_text, # Checked above
# Checked above 'staff_access': True,
'staff_access': True, }) }
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def combined_notifications(request, course_id): def combined_notifications(request, course_id):
...@@ -322,7 +291,7 @@ def take_action_on_flags(request, course_id): ...@@ -322,7 +291,7 @@ def take_action_on_flags(request, course_id):
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type) response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id)) log.exception("Error saving calibration grade, submission_id: {0}, submission_key: {1}, grader_id: {2}".format(submission_id, submission_key, grader_id))
return _err_response('Could not connect to grading service') return _err_response('Could not connect to grading service')
...@@ -419,7 +419,6 @@ main_vendor_js = [ ...@@ -419,7 +419,6 @@ main_vendor_js = [
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee')) discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/open_ended/**/*.coffee')) open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/open_ended/**/*.coffee'))
PIPELINE_CSS = { PIPELINE_CSS = {
...@@ -451,7 +450,7 @@ PIPELINE_JS = { ...@@ -451,7 +450,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') + set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) - rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js + open_ended_js) set(courseware_js + discussion_js + staff_grading_js + open_ended_js)
) + [ ) + [
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
...@@ -481,10 +480,6 @@ PIPELINE_JS = { ...@@ -481,10 +480,6 @@ PIPELINE_JS = {
'source_filenames': staff_grading_js, 'source_filenames': staff_grading_js,
'output_filename': 'js/staff_grading.js' 'output_filename': 'js/staff_grading.js'
}, },
'peer_grading' : {
'source_filenames': peer_grading_js,
'output_filename': 'js/peer_grading.js'
},
'open_ended' : { 'open_ended' : {
'source_filenames': open_ended_js, 'source_filenames': open_ended_js,
'output_filename': 'js/open_ended.js' 'output_filename': 'js/open_ended.js'
......
...@@ -120,7 +120,7 @@ div.peer-grading{ ...@@ -120,7 +120,7 @@ div.peer-grading{
margin-right:20px; margin-right:20px;
> div > div
{ {
padding: 10px; padding: 2px;
margin: 0px; margin: 0px;
background: #eee; background: #eee;
height: 10em; height: 10em;
......
<%inherit file="/main.html" /> <section class="container peer-grading-container">
<%block name="bodyclass">${course.css_class}</%block> <div class="peer-grading" data-ajax-url="${ajax_url}" data-use-single-location="${use_single_location}">
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} Peer Grading</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='peer_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}">
<div class="error-container">${error_text}</div> <div class="error-container">${error_text}</div>
<h1>Peer Grading</h1> <h1>Peer Grading</h1>
<h2>Instructions</h2> <h2>Instructions</h2>
...@@ -38,7 +22,7 @@ ...@@ -38,7 +22,7 @@
%for problem in problem_list: %for problem in problem_list:
<tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}"> <tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}">
<td class="problem-name"> <td class="problem-name">
<a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']}</a> <a href="#problem" data-location="${problem['location']}" class="problem-button">${problem['problem_name']}</a>
</td> </td>
<td> <td>
${problem['num_graded']} ${problem['num_graded']}
......
<section class="container peer-grading-container">
<%inherit file="/main.html" /> <div class="peer-grading" data-ajax-url="${ajax_url}" data-location="${problem_location}" data-use-single-location="${use_single_location}">
<%block name="bodyclass">${course.css_class}</%block>
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} Peer Grading.</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='peer_grading'" />
<%block name="js_extra">
<%static:js group='peer_grading'/>
</%block>
<section class="container">
<div class="peer-grading" data-ajax_url="${ajax_url}" data-location="${problem_location}">
<div class="error-container"></div> <div class="error-container"></div>
<section class="content-panel"> <section class="content-panel">
......
...@@ -268,23 +268,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -268,23 +268,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$',
'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'), 'open_ended_grading.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
# Peer Grading
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/problem$',
'open_ended_grading.views.peer_grading_problem', name='peer_grading_problem'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/get_next_submission$',
'open_ended_grading.peer_grading_service.get_next_submission', name='peer_grading_get_next_submission'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/show_calibration_essay$',
'open_ended_grading.peer_grading_service.show_calibration_essay', name='peer_grading_show_calibration_essay'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/is_student_calibrated$',
'open_ended_grading.peer_grading_service.is_student_calibrated', name='peer_grading_is_student_calibrated'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_grade$',
'open_ended_grading.peer_grading_service.save_grade', name='peer_grading_save_grade'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading/save_calibration_essay$',
'open_ended_grading.peer_grading_service.save_calibration_essay', name='peer_grading_save_calibration_essay'),
# Open Ended problem list # Open Ended problem list
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
'open_ended_grading.views.student_problem_list', name='open_ended_problems'), 'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
...@@ -317,6 +300,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -317,6 +300,9 @@ if settings.COURSEWARE_ENABLED:
# Open Ended Notifications # Open Ended Notifications
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_notifications$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_notifications$',
'open_ended_grading.views.combined_notifications', name='open_ended_notifications'), 'open_ended_grading.views.combined_notifications', name='open_ended_notifications'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/peer_grading$',
'open_ended_grading.views.peer_grading', name='peer_grading'),
) )
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment