Remove 'open_ended_grading' djangoapp & URLs (ORA1)
This commit is contained in:
@@ -1016,7 +1016,6 @@ ADVANCED_COMPONENT_TYPES = [
|
||||
'rate', # Allows up-down voting of course content. See https://github.com/pmitros/RateXBlock
|
||||
|
||||
'split_test',
|
||||
'combinedopenended',
|
||||
'peergrading',
|
||||
'notes',
|
||||
'schoolyourself_review',
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.open_ended_grading_classes import peer_grading_service
|
||||
from xmodule.open_ended_grading_classes.controller_query_service import ControllerQueryService
|
||||
|
||||
from courseware.access import has_access
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from student.models import unique_id_for_user
|
||||
from util.cache import cache
|
||||
|
||||
from .staff_grading_service import StaffGradingService
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_CACHE_TIME = 300
|
||||
KEY_PREFIX = "open_ended_"
|
||||
|
||||
NOTIFICATION_TYPES = (
|
||||
('student_needs_to_peer_grade', 'peer_grading', 'Peer Grading'),
|
||||
('staff_needs_to_grade', 'staff_grading', 'Staff Grading'),
|
||||
('new_student_grading_to_view', 'open_ended_problems', 'Problems you have submitted'),
|
||||
('flagged_submissions_exist', 'open_ended_flagged_problems', 'Flagged Submissions')
|
||||
)
|
||||
|
||||
|
||||
def staff_grading_notifications(course, user):
|
||||
staff_gs = StaffGradingService(settings.OPEN_ENDED_GRADING_INTERFACE)
|
||||
pending_grading = False
|
||||
img_path = ""
|
||||
course_id = course.id
|
||||
student_id = unique_id_for_user(user)
|
||||
notification_type = "staff"
|
||||
|
||||
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
|
||||
if success:
|
||||
return notification_dict
|
||||
|
||||
try:
|
||||
notifications = json.loads(staff_gs.get_notifications(course_id))
|
||||
if notifications['success']:
|
||||
if notifications['staff_needs_to_grade']:
|
||||
pending_grading = True
|
||||
except:
|
||||
#Non catastrophic error, so no real action
|
||||
notifications = {}
|
||||
#This is a dev_facing_error
|
||||
log.info(
|
||||
"Problem with getting notifications from staff grading service for course {0} user {1}.".format(course_id,
|
||||
student_id))
|
||||
|
||||
if pending_grading:
|
||||
img_path = "/static/images/grading_notification.png"
|
||||
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
|
||||
|
||||
return notification_dict
|
||||
|
||||
|
||||
def peer_grading_notifications(course, user):
|
||||
peer_gs = peer_grading_service.PeerGradingService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string)
|
||||
pending_grading = False
|
||||
img_path = ""
|
||||
course_id = course.id
|
||||
student_id = unique_id_for_user(user)
|
||||
notification_type = "peer"
|
||||
|
||||
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
|
||||
if success:
|
||||
return notification_dict
|
||||
|
||||
try:
|
||||
notifications = json.loads(peer_gs.get_notifications(course_id, student_id))
|
||||
if notifications['success']:
|
||||
if notifications['student_needs_to_peer_grade']:
|
||||
pending_grading = True
|
||||
except:
|
||||
#Non catastrophic error, so no real action
|
||||
notifications = {}
|
||||
#This is a dev_facing_error
|
||||
log.info(
|
||||
"Problem with getting notifications from peer grading service for course {0} user {1}.".format(course_id,
|
||||
student_id))
|
||||
if pending_grading:
|
||||
img_path = "/static/images/grading_notification.png"
|
||||
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
|
||||
|
||||
return notification_dict
|
||||
|
||||
|
||||
def combined_notifications(course, user):
|
||||
"""
|
||||
Show notifications to a given user for a given course. Get notifications from the cache if possible,
|
||||
or from the grading controller server if not.
|
||||
@param course: The course object for which we are getting notifications
|
||||
@param user: The user object for which we are getting notifications
|
||||
@return: A dictionary with boolean pending_grading (true if there is pending grading), img_path (for notification
|
||||
image), and response (actual response from grading controller server).
|
||||
"""
|
||||
#Set up return values so that we can return them for error cases
|
||||
pending_grading = False
|
||||
img_path = ""
|
||||
notifications = {}
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#We don't want to show anonymous users anything.
|
||||
if not user.is_authenticated():
|
||||
return notification_dict
|
||||
|
||||
#Initialize controller query service using our mock system
|
||||
controller_qs = ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string)
|
||||
student_id = unique_id_for_user(user)
|
||||
user_is_staff = bool(has_access(user, 'staff', course))
|
||||
course_id = course.id
|
||||
notification_type = "combined"
|
||||
|
||||
#See if we have a stored value in the cache
|
||||
success, notification_dict = get_value_from_cache(student_id, course_id, notification_type)
|
||||
if success:
|
||||
return notification_dict
|
||||
|
||||
#Get the time of the last login of the user
|
||||
last_login = user.last_login
|
||||
last_time_viewed = last_login - datetime.timedelta(seconds=(NOTIFICATION_CACHE_TIME + 60))
|
||||
|
||||
try:
|
||||
#Get the notifications from the grading controller
|
||||
notifications = controller_qs.check_combined_notifications(
|
||||
course.id,
|
||||
student_id,
|
||||
user_is_staff,
|
||||
last_time_viewed,
|
||||
)
|
||||
if notifications.get('success'):
|
||||
if (notifications.get('staff_needs_to_grade') or
|
||||
notifications.get('student_needs_to_peer_grade')):
|
||||
pending_grading = True
|
||||
except:
|
||||
#Non catastrophic error, so no real action
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
u"Problem with getting notifications from controller query service for course {0} user {1}.".format(
|
||||
course_id, student_id))
|
||||
|
||||
if pending_grading:
|
||||
img_path = "/static/images/grading_notification.png"
|
||||
|
||||
notification_dict = {'pending_grading': pending_grading, 'img_path': img_path, 'response': notifications}
|
||||
|
||||
#Store the notifications in the cache
|
||||
set_value_in_cache(student_id, course_id, notification_type, notification_dict)
|
||||
|
||||
return notification_dict
|
||||
|
||||
|
||||
def get_value_from_cache(student_id, course_id, notification_type):
|
||||
key_name = create_key_name(student_id, course_id, notification_type)
|
||||
success, value = _get_value_from_cache(key_name)
|
||||
return success, value
|
||||
|
||||
|
||||
def set_value_in_cache(student_id, course_id, notification_type, value):
|
||||
key_name = create_key_name(student_id, course_id, notification_type)
|
||||
_set_value_in_cache(key_name, value)
|
||||
|
||||
|
||||
def create_key_name(student_id, course_id, notification_type):
|
||||
key_name = u"{prefix}{type}_{course}_{student}".format(
|
||||
prefix=KEY_PREFIX,
|
||||
type=notification_type,
|
||||
course=course_id,
|
||||
student=student_id,
|
||||
)
|
||||
return key_name
|
||||
|
||||
|
||||
def _get_value_from_cache(key_name):
|
||||
value = cache.get(key_name)
|
||||
success = False
|
||||
if value is None:
|
||||
return success, value
|
||||
try:
|
||||
value = json.loads(value)
|
||||
success = True
|
||||
except:
|
||||
pass
|
||||
return success, value
|
||||
|
||||
|
||||
def _set_value_in_cache(key_name, value):
|
||||
cache.set(key_name, json.dumps(value), NOTIFICATION_CACHE_TIME)
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
LMS part of instructor grading:
|
||||
|
||||
- views + ajax handling
|
||||
- calls the instructor grading service
|
||||
"""
|
||||
|
||||
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 "<b>Instructor grading!</b>"
|
||||
# context = {}
|
||||
# return render_to_string('courseware/instructor_grading_view.html', context)
|
||||
@@ -1,444 +0,0 @@
|
||||
"""
|
||||
This module provides views that proxy to the staff grading backend service.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.open_ended_grading_classes.grading_service_module import GradingService, GradingServiceError
|
||||
|
||||
from courseware.access import has_access
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
from open_ended_grading.utils import does_location_exist
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
STAFF_ERROR_MESSAGE = _(
|
||||
u'Could not contact the external grading server. Please contact the '
|
||||
u'development team at {email}.'
|
||||
).format(
|
||||
email=u'<a href="mailto:{tech_support_email}>{tech_support_email}</a>'.format(
|
||||
tech_support_email=settings.TECH_SUPPORT_EMAIL
|
||||
)
|
||||
)
|
||||
MAX_ALLOWED_FEEDBACK_LENGTH = 5000
|
||||
|
||||
|
||||
class MockStaffGradingService(object):
|
||||
"""
|
||||
A simple mockup of a staff grading service, testing.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.cnt = 0
|
||||
|
||||
def get_next(self, course_id, location, grader_id):
|
||||
self.cnt += 1
|
||||
return {'success': True,
|
||||
'submission_id': self.cnt,
|
||||
'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
|
||||
'num_graded': 3,
|
||||
'min_for_ml': 5,
|
||||
'num_pending': 4,
|
||||
'prompt': 'This is a fake prompt',
|
||||
'ml_error_info': 'ML info',
|
||||
'max_score': 2 + self.cnt % 3,
|
||||
'rubric': 'A rubric'}
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
self.cnt += 1
|
||||
return {
|
||||
'success': True,
|
||||
'problem_list': [
|
||||
json.dumps({
|
||||
'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
|
||||
'problem_name': "Problem 1",
|
||||
'num_graded': 3,
|
||||
'num_pending': 5,
|
||||
'min_for_ml': 10,
|
||||
}),
|
||||
json.dumps({
|
||||
'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
|
||||
'problem_name': "Problem 2",
|
||||
'num_graded': 1,
|
||||
'num_pending': 5,
|
||||
'min_for_ml': 10,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
|
||||
submission_flagged):
|
||||
return self.get_next(course_id, 'fake location', grader_id)
|
||||
|
||||
|
||||
class StaffGradingService(GradingService):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
"""
|
||||
|
||||
METRIC_NAME = 'edxapp.open_ended_grading.staff_grading_service'
|
||||
|
||||
def __init__(self, config):
|
||||
config['render_template'] = render_to_string
|
||||
super(StaffGradingService, self).__init__(config)
|
||||
self.url = config['url'] + config['staff_grading']
|
||||
self.login_url = self.url + '/login/'
|
||||
self.get_next_url = self.url + '/get_next_submission/'
|
||||
self.save_grade_url = self.url + '/save_grade/'
|
||||
self.get_problem_list_url = self.url + '/get_problem_list/'
|
||||
self.get_notifications_url = self.url + "/get_notifications/"
|
||||
|
||||
def get_problem_list(self, course_id, grader_id):
|
||||
"""
|
||||
Get the list of problems for a given course.
|
||||
|
||||
Args:
|
||||
course_id: course id that we want the problems of
|
||||
grader_id: who is grading this? The anonymous user_id of the grader.
|
||||
|
||||
Returns:
|
||||
dict with the response from the service. (Deliberately not
|
||||
writing out the fields here--see the docs on the staff_grading view
|
||||
in the grading_controller repo)
|
||||
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
params = {'course_id': course_id.to_deprecated_string(), 'grader_id': grader_id}
|
||||
result = self.get(self.get_problem_list_url, params)
|
||||
tags = [u'course_id:{}'.format(course_id)]
|
||||
self._record_result('get_problem_list', result, tags)
|
||||
dog_stats_api.histogram(
|
||||
self._metric_name('get_problem_list.result.length'),
|
||||
len(result.get('problem_list', []))
|
||||
)
|
||||
return result
|
||||
|
||||
def get_next(self, course_id, location, grader_id):
|
||||
"""
|
||||
Get the next thing to grade.
|
||||
|
||||
Args:
|
||||
course_id: the course that this problem belongs to
|
||||
location: location of the problem that we are grading and would like the
|
||||
next submission for
|
||||
grader_id: who is grading this? The anonymous user_id of the grader.
|
||||
|
||||
Returns:
|
||||
dict with the response from the service. (Deliberately not
|
||||
writing out the fields here--see the docs on the staff_grading view
|
||||
in the grading_controller repo)
|
||||
|
||||
Raises:
|
||||
GradingServiceError: something went wrong with the connection.
|
||||
"""
|
||||
result = self._render_rubric(
|
||||
self.get(
|
||||
self.get_next_url,
|
||||
params={
|
||||
'location': location.to_deprecated_string(),
|
||||
'grader_id': grader_id
|
||||
}
|
||||
)
|
||||
)
|
||||
tags = [u'course_id:{}'.format(course_id)]
|
||||
self._record_result('get_next', result, tags)
|
||||
return result
|
||||
|
||||
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores,
|
||||
submission_flagged):
|
||||
"""
|
||||
Save a score and feedback for a submission.
|
||||
|
||||
Returns:
|
||||
dict with keys
|
||||
'success': bool
|
||||
'error': error msg, if something went wrong.
|
||||
|
||||
Raises:
|
||||
GradingServiceError if there's a problem connecting.
|
||||
"""
|
||||
data = {'course_id': course_id.to_deprecated_string(),
|
||||
'submission_id': submission_id,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'grader_id': grader_id,
|
||||
'skipped': skipped,
|
||||
'rubric_scores': rubric_scores,
|
||||
'rubric_scores_complete': True,
|
||||
'submission_flagged': submission_flagged}
|
||||
|
||||
result = self._render_rubric(self.post(self.save_grade_url, data=data))
|
||||
tags = [u'course_id:{}'.format(course_id)]
|
||||
self._record_result('save_grade', result, tags)
|
||||
return result
|
||||
|
||||
def get_notifications(self, course_id):
|
||||
params = {'course_id': course_id.to_deprecated_string()}
|
||||
result = self.get(self.get_notifications_url, params)
|
||||
tags = [
|
||||
u'course_id:{}'.format(course_id),
|
||||
u'staff_needs_to_grade:{}'.format(result.get('staff_needs_to_grade'))
|
||||
]
|
||||
self._record_result('get_notifications', result, tags)
|
||||
return result
|
||||
|
||||
|
||||
# don't initialize until staff_grading_service() is called--means that just
|
||||
# importing this file doesn't create objects that may not have the right config
|
||||
_service = None
|
||||
|
||||
|
||||
def staff_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.OPEN_ENDED_GRADING_INTERFACE)
|
||||
|
||||
return _service
|
||||
|
||||
|
||||
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}),
|
||||
content_type="application/json")
|
||||
|
||||
|
||||
def _check_access(user, course_id):
|
||||
"""
|
||||
Raise 404 if user doesn't have staff access to course_id
|
||||
"""
|
||||
if not has_access(user, 'staff', course_id):
|
||||
raise Http404
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_next(request, course_id):
|
||||
"""
|
||||
Get the next thing to grade for course_id and with the location specified
|
||||
in the request.
|
||||
|
||||
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.
|
||||
"""
|
||||
assert isinstance(course_id, basestring)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
|
||||
required = set(['location'])
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
actual = set(request.POST.keys())
|
||||
missing = required - actual
|
||||
if len(missing) > 0:
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
p = request.POST
|
||||
location = course_key.make_usage_key_from_deprecated_string(p['location'])
|
||||
|
||||
return HttpResponse(json.dumps(_get_next(course_key, grader_id, location)),
|
||||
content_type="application/json")
|
||||
|
||||
|
||||
def get_problem_list(request, course_id):
|
||||
"""
|
||||
Get all the problems for the given course id
|
||||
|
||||
Returns a json dict with the following keys:
|
||||
success: bool
|
||||
|
||||
problem_list: a list containing json dicts with the following keys:
|
||||
each dict represents a different problem in the course
|
||||
|
||||
location: the location of the problem
|
||||
|
||||
problem_name: the name of the problem
|
||||
|
||||
num_graded: the number of responses that have been graded
|
||||
|
||||
num_pending: the number of responses that are sitting in the queue
|
||||
|
||||
min_for_ml: the number of responses that need to be graded before
|
||||
the ml can be run
|
||||
|
||||
'error': if success is False, will have an error message with more info.
|
||||
"""
|
||||
assert isinstance(course_id, basestring)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
try:
|
||||
response = staff_grading_service().get_problem_list(course_key, unique_id_for_user(request.user))
|
||||
|
||||
# If 'problem_list' is in the response, then we got a list of problems from the ORA server.
|
||||
# If it is not, then ORA could not find any problems.
|
||||
if 'problem_list' in response:
|
||||
problem_list = response['problem_list']
|
||||
else:
|
||||
problem_list = []
|
||||
# Make an error messages to reflect that we could not find anything to grade.
|
||||
response['error'] = _(
|
||||
u'Cannot find any open response problems in this course. '
|
||||
u'Have you submitted answers to any open response assessment questions? '
|
||||
u'If not, please do so and return to this page.'
|
||||
)
|
||||
valid_problem_list = []
|
||||
for i in xrange(len(problem_list)):
|
||||
# Needed to ensure that the 'location' key can be accessed.
|
||||
try:
|
||||
problem_list[i] = json.loads(problem_list[i])
|
||||
except Exception:
|
||||
pass
|
||||
if does_location_exist(course_key.make_usage_key_from_deprecated_string(problem_list[i]['location'])):
|
||||
valid_problem_list.append(problem_list[i])
|
||||
response['problem_list'] = valid_problem_list
|
||||
response = json.dumps(response)
|
||||
|
||||
return HttpResponse(response, content_type="application/json")
|
||||
except GradingServiceError:
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
"Error from staff grading service in open "
|
||||
"ended grading. server url: {0}".format(staff_grading_service().url)
|
||||
)
|
||||
#This is a staff_facing_error
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE}))
|
||||
|
||||
|
||||
def _get_next(course_id, grader_id, location):
|
||||
"""
|
||||
Implementation of get_next (also called from save_grade) -- returns a json string
|
||||
"""
|
||||
try:
|
||||
return staff_grading_service().get_next(course_id, location, grader_id)
|
||||
except GradingServiceError:
|
||||
#This is a dev facing error
|
||||
log.exception(
|
||||
"Error from staff grading service in open "
|
||||
"ended grading. server url: {0}".format(staff_grading_service().url)
|
||||
)
|
||||
#This is a staff_facing_error
|
||||
return json.dumps({'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE})
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
_check_access(request.user, course_key)
|
||||
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
p = request.POST
|
||||
required = set(['score', 'feedback', 'submission_id', 'location', 'submission_flagged'])
|
||||
skipped = 'skipped' in p
|
||||
#If the instructor has skipped grading the submission, then there will not be any rubric scores.
|
||||
#Only add in the rubric scores if the instructor has not skipped.
|
||||
if not skipped:
|
||||
required.add('rubric_scores[]')
|
||||
actual = set(p.keys())
|
||||
missing = required - actual
|
||||
if len(missing) > 0:
|
||||
return _err_response('Missing required keys {0}'.format(
|
||||
', '.join(missing)))
|
||||
|
||||
success, message = check_feedback_length(p)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
|
||||
grader_id = unique_id_for_user(request.user)
|
||||
|
||||
location = course_key.make_usage_key_from_deprecated_string(p['location'])
|
||||
|
||||
try:
|
||||
result = staff_grading_service().save_grade(course_key,
|
||||
grader_id,
|
||||
p['submission_id'],
|
||||
p['score'],
|
||||
p['feedback'],
|
||||
skipped,
|
||||
p.getlist('rubric_scores[]'),
|
||||
p['submission_flagged'])
|
||||
except GradingServiceError:
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
"Error saving grade in the staff grading interface in open ended grading. Request: {0} Course ID: {1}".format(
|
||||
request, course_id))
|
||||
#This is a staff_facing_error
|
||||
return _err_response(STAFF_ERROR_MESSAGE)
|
||||
except ValueError:
|
||||
#This is a dev_facing_error
|
||||
log.exception(
|
||||
"save_grade returned broken json in the staff grading interface in open ended grading: {0}".format(
|
||||
result_json))
|
||||
#This is a staff_facing_error
|
||||
return _err_response(STAFF_ERROR_MESSAGE)
|
||||
|
||||
if not result.get('success', False):
|
||||
#This is a dev_facing_error
|
||||
log.warning(
|
||||
'Got success=False from staff grading service in open ended grading. Response: {0}'.format(result_json))
|
||||
return _err_response(STAFF_ERROR_MESSAGE)
|
||||
|
||||
# Ok, save_grade seemed to work. Get the next submission to grade.
|
||||
return HttpResponse(json.dumps(_get_next(course_id, grader_id, location)),
|
||||
content_type="application/json")
|
||||
|
||||
|
||||
def check_feedback_length(data):
|
||||
feedback = data.get("feedback")
|
||||
if feedback and len(feedback) > MAX_ALLOWED_FEEDBACK_LENGTH:
|
||||
return False, "Feedback is too long, Max length is {0} characters.".format(
|
||||
MAX_ALLOWED_FEEDBACK_LENGTH
|
||||
)
|
||||
else:
|
||||
return True, ""
|
||||
@@ -1,588 +0,0 @@
|
||||
"""
|
||||
Tests for open ended grading interfaces
|
||||
|
||||
./manage.py lms --settings test test lms/djangoapps/open_ended_grading
|
||||
"""
|
||||
import ddt
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from mock import MagicMock, patch, Mock
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from config_models.models import cache
|
||||
from courseware.tests import factories
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem
|
||||
from student.roles import CourseStaffRole
|
||||
from student.models import unique_id_for_user
|
||||
from xblock_django.models import XBlockDisableConfig
|
||||
from xmodule import peer_grading_module
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_TOY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
|
||||
from xmodule.tests import test_util_open_ended
|
||||
|
||||
from open_ended_grading import staff_grading_service, views, utils
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmptyStaffGradingService(object):
|
||||
"""
|
||||
A staff grading service that does not return a problem list from get_problem_list.
|
||||
Used for testing to see if error message for empty problem list is correctly displayed.
|
||||
"""
|
||||
|
||||
def get_problem_list(self, course_id, user_id):
|
||||
"""
|
||||
Return a staff grading response that is missing a problem list key.
|
||||
"""
|
||||
return {'success': True, 'error': 'No problems found.'}
|
||||
|
||||
|
||||
def make_instructor(course, user_email):
|
||||
"""
|
||||
Makes a given user an instructor in a course.
|
||||
"""
|
||||
CourseStaffRole(course.id).add_users(User.objects.get(email=user_email))
|
||||
|
||||
|
||||
class StudentProblemListMockQuery(object):
|
||||
"""
|
||||
Mock controller query service for testing student problem list functionality.
|
||||
"""
|
||||
def get_grading_status_list(self, *args, **kwargs):
|
||||
"""
|
||||
Get a mock grading status list with locations from the open_ended test course.
|
||||
@returns: grading status message dictionary.
|
||||
"""
|
||||
return {
|
||||
"version": 1,
|
||||
"problem_list": [
|
||||
{
|
||||
"problem_name": "Test1",
|
||||
"grader_type": "IN",
|
||||
"eta_available": True,
|
||||
"state": "Finished",
|
||||
"eta": 259200,
|
||||
"location": "i4x://edX/open_ended/combinedopenended/SampleQuestion1Attempt"
|
||||
},
|
||||
{
|
||||
"problem_name": "Test2",
|
||||
"grader_type": "NA",
|
||||
"eta_available": True,
|
||||
"state": "Waiting to be Graded",
|
||||
"eta": 259200,
|
||||
"location": "i4x://edX/open_ended/combinedopenended/SampleQuestion"
|
||||
},
|
||||
{
|
||||
"problem_name": "Test3",
|
||||
"grader_type": "PE",
|
||||
"eta_available": True,
|
||||
"state": "Waiting to be Graded",
|
||||
"eta": 259200,
|
||||
"location": "i4x://edX/open_ended/combinedopenended/SampleQuestion454"
|
||||
},
|
||||
],
|
||||
"success": True
|
||||
}
|
||||
|
||||
|
||||
class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
MODULESTORE = TEST_DATA_MIXED_TOY_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
super(TestStaffGradingService, self).setUp()
|
||||
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 = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string()
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
|
||||
make_instructor(self.toy, self.instructor)
|
||||
|
||||
self.mock_service = staff_grading_service.staff_grading_service()
|
||||
|
||||
self.logout()
|
||||
|
||||
def test_access(self):
|
||||
"""
|
||||
Make sure only staff have access.
|
||||
"""
|
||||
self.login(self.student, self.password)
|
||||
|
||||
# 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.to_deprecated_string()})
|
||||
self.assert_request_status_code(404, url, method="GET")
|
||||
self.assert_request_status_code(404, url, method="POST")
|
||||
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
data = {'location': self.location_string}
|
||||
|
||||
response = self.assert_request_status_code(200, url, method="POST", data=data)
|
||||
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertTrue(content['success'])
|
||||
self.assertEquals(content['submission_id'], self.mock_service.cnt)
|
||||
self.assertIsNotNone(content['submission'])
|
||||
self.assertIsNotNone(content['num_graded'])
|
||||
self.assertIsNotNone(content['min_for_ml'])
|
||||
self.assertIsNotNone(content['num_pending'])
|
||||
self.assertIsNotNone(content['prompt'])
|
||||
self.assertIsNotNone(content['ml_error_info'])
|
||||
self.assertIsNotNone(content['max_score'])
|
||||
self.assertIsNotNone(content['rubric'])
|
||||
|
||||
def save_grade_base(self, skip=False):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
|
||||
data = {'score': '12',
|
||||
'feedback': 'great!',
|
||||
'submission_id': '123',
|
||||
'location': self.location_string,
|
||||
'submission_flagged': "true",
|
||||
'rubric_scores[]': ['1', '2']}
|
||||
if skip:
|
||||
data.update({'skipped': True})
|
||||
|
||||
response = self.assert_request_status_code(200, url, method="POST", data=data)
|
||||
content = json.loads(response.content)
|
||||
self.assertTrue(content['success'], str(content))
|
||||
self.assertEquals(content['submission_id'], self.mock_service.cnt)
|
||||
|
||||
def test_save_grade(self):
|
||||
self.save_grade_base(skip=False)
|
||||
|
||||
def test_save_grade_skip(self):
|
||||
self.save_grade_base(skip=True)
|
||||
|
||||
def test_get_problem_list(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
data = {}
|
||||
|
||||
response = self.assert_request_status_code(200, url, method="POST", data=data)
|
||||
content = json.loads(response.content)
|
||||
|
||||
self.assertTrue(content['success'])
|
||||
self.assertEqual(content['problem_list'], [])
|
||||
|
||||
@patch('open_ended_grading.staff_grading_service._service', EmptyStaffGradingService())
|
||||
def test_get_problem_list_missing(self):
|
||||
"""
|
||||
Test to see if a staff grading response missing a problem list is given the appropriate error.
|
||||
Mock the staff grading service to enable the key to be missing.
|
||||
"""
|
||||
|
||||
# Get a valid user object.
|
||||
instructor = User.objects.get(email=self.instructor)
|
||||
# Mock a request object.
|
||||
request = Mock(
|
||||
user=instructor,
|
||||
)
|
||||
# Get the response and load its content.
|
||||
response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content)
|
||||
|
||||
# A valid response will have an "error" key.
|
||||
self.assertTrue('error' in response)
|
||||
# Check that the error text is correct.
|
||||
self.assertIn("Cannot find", response['error'])
|
||||
|
||||
def test_save_grade_with_long_feedback(self):
|
||||
"""
|
||||
Test if feedback is too long save_grade() should return error message.
|
||||
"""
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()})
|
||||
|
||||
data = {
|
||||
'score': '12',
|
||||
'feedback': '',
|
||||
'submission_id': '123',
|
||||
'location': self.location_string,
|
||||
'submission_flagged': "false",
|
||||
'rubric_scores[]': ['1', '2']
|
||||
}
|
||||
|
||||
feedback_fragment = "This is very long feedback."
|
||||
data["feedback"] = feedback_fragment * (
|
||||
(staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH / len(feedback_fragment) + 1)
|
||||
)
|
||||
|
||||
response = self.assert_request_status_code(200, url, method="POST", data=data)
|
||||
content = json.loads(response.content)
|
||||
|
||||
# Should not succeed.
|
||||
self.assertEquals(content['success'], False)
|
||||
self.assertEquals(
|
||||
content['error'],
|
||||
"Feedback is too long, Max length is {0} characters.".format(
|
||||
staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestPeerGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
'''
|
||||
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):
|
||||
super(TestPeerGradingService, self).setUp()
|
||||
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 = SlashSeparatedCourseKey("edX", "toy", "2012_Fall")
|
||||
self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string()
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
location = "i4x://edX/toy/peergrading/init"
|
||||
field_data = DictFieldData({'data': "<peergrading/>", 'location': location, 'category': 'peergrading'})
|
||||
self.mock_service = peer_grading_service.MockPeerGradingService()
|
||||
self.system = LmsModuleSystem(
|
||||
static_url=settings.STATIC_URL,
|
||||
track_function=None,
|
||||
get_module=None,
|
||||
render_template=render_to_string,
|
||||
replace_urls=None,
|
||||
s3_interface=test_util_open_ended.S3_INTERFACE,
|
||||
open_ended_grading_interface=test_util_open_ended.OPEN_ENDED_GRADING_INTERFACE,
|
||||
mixins=settings.XBLOCK_MIXINS,
|
||||
error_descriptor_class=ErrorDescriptor,
|
||||
descriptor_runtime=None,
|
||||
)
|
||||
self.descriptor = peer_grading_module.PeerGradingDescriptor(self.system, field_data, ScopeIds(None, None, None, None))
|
||||
self.descriptor.xmodule_runtime = self.system
|
||||
self.peer_module = self.descriptor
|
||||
self.peer_module.peer_gs = self.mock_service
|
||||
self.logout()
|
||||
|
||||
def test_get_next_submission_success(self):
|
||||
data = {'location': self.location_string}
|
||||
|
||||
response = self.peer_module.get_next_submission(data)
|
||||
content = response
|
||||
|
||||
self.assertTrue(content['success'])
|
||||
self.assertIsNotNone(content['submission_id'])
|
||||
self.assertIsNotNone(content['prompt'])
|
||||
self.assertIsNotNone(content['submission_key'])
|
||||
self.assertIsNotNone(content['max_score'])
|
||||
|
||||
def test_get_next_submission_missing_location(self):
|
||||
data = {}
|
||||
d = self.peer_module.get_next_submission(data)
|
||||
self.assertFalse(d['success'])
|
||||
self.assertEqual(d['error'], "Missing required keys: location")
|
||||
|
||||
def test_save_grade_success(self):
|
||||
data = {
|
||||
'rubric_scores[]': [0, 0],
|
||||
'location': self.location_string,
|
||||
'submission_id': 1,
|
||||
'submission_key': 'fake key',
|
||||
'score': 2,
|
||||
'feedback': 'feedback',
|
||||
'submission_flagged': 'false',
|
||||
'answer_unknown': 'false',
|
||||
'rubric_scores_complete': 'true'
|
||||
}
|
||||
|
||||
qdict = MagicMock()
|
||||
|
||||
def fake_get_item(key):
|
||||
return data[key]
|
||||
|
||||
qdict.__getitem__.side_effect = fake_get_item
|
||||
qdict.getlist = fake_get_item
|
||||
qdict.keys = data.keys
|
||||
|
||||
response = self.peer_module.save_grade(qdict)
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
|
||||
def test_save_grade_missing_keys(self):
|
||||
data = {}
|
||||
d = self.peer_module.save_grade(data)
|
||||
self.assertFalse(d['success'])
|
||||
self.assertTrue(d['error'].find('Missing required keys:') > -1)
|
||||
|
||||
def test_is_calibrated_success(self):
|
||||
data = {'location': self.location_string}
|
||||
response = self.peer_module.is_student_calibrated(data)
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
self.assertTrue('calibrated' in response)
|
||||
|
||||
def test_is_calibrated_failure(self):
|
||||
data = {}
|
||||
response = self.peer_module.is_student_calibrated(data)
|
||||
self.assertFalse(response['success'])
|
||||
self.assertFalse('calibrated' in response)
|
||||
|
||||
def test_show_calibration_essay_success(self):
|
||||
data = {'location': self.location_string}
|
||||
|
||||
response = self.peer_module.show_calibration_essay(data)
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
self.assertIsNotNone(response['submission_id'])
|
||||
self.assertIsNotNone(response['prompt'])
|
||||
self.assertIsNotNone(response['submission_key'])
|
||||
self.assertIsNotNone(response['max_score'])
|
||||
|
||||
def test_show_calibration_essay_missing_key(self):
|
||||
data = {}
|
||||
|
||||
response = self.peer_module.show_calibration_essay(data)
|
||||
|
||||
self.assertFalse(response['success'])
|
||||
self.assertEqual(response['error'], "Missing required keys: location")
|
||||
|
||||
def test_save_calibration_essay_success(self):
|
||||
data = {
|
||||
'rubric_scores[]': [0, 0],
|
||||
'location': self.location_string,
|
||||
'submission_id': 1,
|
||||
'submission_key': 'fake key',
|
||||
'score': 2,
|
||||
'feedback': 'feedback',
|
||||
'submission_flagged': 'false'
|
||||
}
|
||||
|
||||
qdict = MagicMock()
|
||||
|
||||
def fake_get_item(key):
|
||||
return data[key]
|
||||
|
||||
qdict.__getitem__.side_effect = fake_get_item
|
||||
qdict.getlist = fake_get_item
|
||||
qdict.keys = data.keys
|
||||
|
||||
response = self.peer_module.save_calibration_essay(qdict)
|
||||
self.assertTrue(response['success'])
|
||||
self.assertTrue('actual_score' in response)
|
||||
|
||||
def test_save_calibration_essay_missing_keys(self):
|
||||
data = {}
|
||||
response = self.peer_module.save_calibration_essay(data)
|
||||
self.assertFalse(response['success'])
|
||||
self.assertTrue(response['error'].find('Missing required keys:') > -1)
|
||||
self.assertFalse('actual_score' in response)
|
||||
|
||||
def test_save_grade_with_long_feedback(self):
|
||||
"""
|
||||
Test if feedback is too long save_grade() should return error message.
|
||||
"""
|
||||
data = {
|
||||
'rubric_scores[]': [0, 0],
|
||||
'location': self.location_string,
|
||||
'submission_id': 1,
|
||||
'submission_key': 'fake key',
|
||||
'score': 2,
|
||||
'feedback': '',
|
||||
'submission_flagged': 'false',
|
||||
'answer_unknown': 'false',
|
||||
'rubric_scores_complete': 'true'
|
||||
}
|
||||
|
||||
feedback_fragment = "This is very long feedback."
|
||||
data["feedback"] = feedback_fragment * (
|
||||
(staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH / len(feedback_fragment) + 1)
|
||||
)
|
||||
|
||||
response_dict = self.peer_module.save_grade(data)
|
||||
|
||||
# Should not succeed.
|
||||
self.assertEquals(response_dict['success'], False)
|
||||
self.assertEquals(
|
||||
response_dict['error'],
|
||||
"Feedback is too long, Max length is {0} characters.".format(
|
||||
staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestPanel(ModuleStoreTestCase):
|
||||
"""
|
||||
Run tests on the open ended panel
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestPanel, self).setUp()
|
||||
self.user = factories.UserFactory()
|
||||
store = modulestore()
|
||||
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
|
||||
self.course = course_items[0]
|
||||
self.course_key = self.course.id
|
||||
|
||||
def test_open_ended_panel(self):
|
||||
"""
|
||||
Test to see if the peer grading module in the demo course is found
|
||||
@return:
|
||||
"""
|
||||
found_module, peer_grading_module = views.find_peer_grading_module(self.course)
|
||||
self.assertTrue(found_module)
|
||||
|
||||
@patch(
|
||||
'open_ended_grading.utils.create_controller_query_service',
|
||||
Mock(
|
||||
return_value=controller_query_service.MockControllerQueryService(
|
||||
settings.OPEN_ENDED_GRADING_INTERFACE,
|
||||
utils.render_to_string
|
||||
)
|
||||
)
|
||||
)
|
||||
def test_problem_list(self):
|
||||
"""
|
||||
Ensure that the problem list from the grading controller server can be rendered properly locally
|
||||
@return:
|
||||
"""
|
||||
request = RequestFactory().get(
|
||||
reverse("open_ended_problems", kwargs={'course_id': self.course_key})
|
||||
)
|
||||
request.user = self.user
|
||||
|
||||
mako_middleware_process_request(request)
|
||||
response = views.student_problem_list(request, self.course.id.to_deprecated_string())
|
||||
self.assertRegexpMatches(response.content, "Here is a list of open ended problems for this course.")
|
||||
|
||||
|
||||
class TestPeerGradingFound(ModuleStoreTestCase):
|
||||
"""
|
||||
Test to see if peer grading modules can be found properly.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestPeerGradingFound, self).setUp()
|
||||
self.user = factories.UserFactory()
|
||||
store = modulestore()
|
||||
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended_nopath']) # pylint: disable=maybe-no-member
|
||||
self.course = course_items[0]
|
||||
self.course_key = self.course.id
|
||||
|
||||
def test_peer_grading_nopath(self):
|
||||
"""
|
||||
The open_ended_nopath course contains a peer grading module with no path to it.
|
||||
Ensure that the exception is caught.
|
||||
"""
|
||||
|
||||
found, url = views.find_peer_grading_module(self.course)
|
||||
self.assertEqual(found, False)
|
||||
|
||||
|
||||
class TestStudentProblemList(ModuleStoreTestCase):
|
||||
"""
|
||||
Test if the student problem list correctly fetches and parses problems.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestStudentProblemList, self).setUp()
|
||||
|
||||
# Load an open ended course with several problems.
|
||||
self.user = factories.UserFactory()
|
||||
store = modulestore()
|
||||
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
|
||||
self.course = course_items[0]
|
||||
self.course_key = self.course.id
|
||||
|
||||
# Enroll our user in our course and make them an instructor.
|
||||
make_instructor(self.course, self.user.email)
|
||||
|
||||
@patch(
|
||||
'open_ended_grading.utils.create_controller_query_service',
|
||||
Mock(return_value=StudentProblemListMockQuery())
|
||||
)
|
||||
def test_get_problem_list(self):
|
||||
"""
|
||||
Test to see if the StudentProblemList class can get and parse a problem list from ORA.
|
||||
Mock the get_grading_status_list function using StudentProblemListMockQuery.
|
||||
"""
|
||||
# Initialize a StudentProblemList object.
|
||||
student_problem_list = utils.StudentProblemList(self.course.id, unique_id_for_user(self.user))
|
||||
# Get the initial problem list from ORA.
|
||||
success = student_problem_list.fetch_from_grading_service()
|
||||
# Should be successful, and we should have three problems. See mock class for details.
|
||||
self.assertTrue(success)
|
||||
self.assertEqual(len(student_problem_list.problem_list), 3)
|
||||
|
||||
# See if the problem locations are valid.
|
||||
valid_problems = student_problem_list.add_problem_data(reverse('courses'))
|
||||
# One location is invalid, so we should now have two.
|
||||
self.assertEqual(len(valid_problems), 2)
|
||||
# Ensure that human names are being set properly.
|
||||
self.assertEqual(valid_problems[0]['grader_type_display_name'], "Instructor Assessment")
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestTabs(ModuleStoreTestCase):
|
||||
"""
|
||||
Test tabs.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestTabs, self).setUp()
|
||||
self.course = CourseFactory(advanced_modules=('combinedopenended'))
|
||||
self.addCleanup(lambda: self._enable_xblock_disable_config(False))
|
||||
|
||||
def _enable_xblock_disable_config(self, enabled):
|
||||
""" Enable or disable xblocks disable. """
|
||||
config = XBlockDisableConfig.current()
|
||||
config.enabled = enabled
|
||||
config.disabled_blocks = "\n".join(('combinedopenended', 'peergrading'))
|
||||
config.save()
|
||||
cache.clear()
|
||||
|
||||
@ddt.data(
|
||||
views.StaffGradingTab,
|
||||
views.PeerGradingTab,
|
||||
views.OpenEndedGradingTab,
|
||||
)
|
||||
def test_tabs_enabled(self, tab):
|
||||
self.assertTrue(tab.is_enabled(self.course))
|
||||
|
||||
@ddt.data(
|
||||
views.StaffGradingTab,
|
||||
views.PeerGradingTab,
|
||||
views.OpenEndedGradingTab,
|
||||
)
|
||||
def test_tabs_disabled(self, tab):
|
||||
self._enable_xblock_disable_config(True)
|
||||
self.assertFalse(tab.is_enabled(self.course))
|
||||
@@ -1,171 +0,0 @@
|
||||
import logging
|
||||
from urllib import urlencode
|
||||
|
||||
from xmodule.modulestore import search
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
|
||||
from xmodule.open_ended_grading_classes.controller_query_service import ControllerQueryService
|
||||
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
GRADER_DISPLAY_NAMES = {
|
||||
'ML': _("AI Assessment"),
|
||||
'PE': _("Peer Assessment"),
|
||||
'NA': _("Not yet available"),
|
||||
'BC': _("Automatic Checker"),
|
||||
'IN': _("Instructor Assessment"),
|
||||
}
|
||||
|
||||
STUDENT_ERROR_MESSAGE = _("Error occurred while contacting the grading service. Please notify course staff.")
|
||||
STAFF_ERROR_MESSAGE = _("Error occurred while contacting the grading service. Please notify your edX point of contact.")
|
||||
|
||||
|
||||
def generate_problem_url(problem_url_parts, base_course_url):
|
||||
"""
|
||||
From a list of problem url parts generated by search.path_to_location and a base course url, generates a url to a problem
|
||||
@param problem_url_parts: Output of search.path_to_location
|
||||
@param base_course_url: Base url of a given course
|
||||
@return: A path to the problem
|
||||
"""
|
||||
activate_block_id = problem_url_parts[-1]
|
||||
problem_url_parts = problem_url_parts[0:-1]
|
||||
problem_url = base_course_url + "/"
|
||||
for i, part in enumerate(problem_url_parts):
|
||||
if part is not None:
|
||||
# This is the course_key. We need to turn it into its deprecated
|
||||
# form.
|
||||
if i == 0:
|
||||
part = part.to_deprecated_string()
|
||||
# This is placed between the course id and the rest of the url.
|
||||
if i == 1:
|
||||
problem_url += "courseware/"
|
||||
problem_url += part + "/"
|
||||
problem_url += '?{}'.format(urlencode({'activate_block_id': unicode(activate_block_id)}))
|
||||
return problem_url
|
||||
|
||||
|
||||
def does_location_exist(usage_key):
|
||||
"""
|
||||
Checks to see if a valid module exists at a given location (ie has not been deleted)
|
||||
course_id - string course id
|
||||
location - string location
|
||||
"""
|
||||
try:
|
||||
search.path_to_location(modulestore(), usage_key)
|
||||
return True
|
||||
except ItemNotFoundError:
|
||||
# If the problem cannot be found at the location received from the grading controller server,
|
||||
# it has been deleted by the course author.
|
||||
return False
|
||||
except NoPathToItem:
|
||||
# If the problem can be found, but there is no path to it, then we assume it is a draft.
|
||||
# Log a warning in any case.
|
||||
log.warn("Got an unexpected NoPathToItem error in staff grading with location %s. "
|
||||
"This is ok if it is a draft; ensure that the location is valid.", usage_key)
|
||||
return False
|
||||
|
||||
|
||||
def create_controller_query_service():
|
||||
"""
|
||||
Return an instance of a service that can query edX ORA.
|
||||
"""
|
||||
return ControllerQueryService(settings.OPEN_ENDED_GRADING_INTERFACE, render_to_string)
|
||||
|
||||
|
||||
class StudentProblemList(object):
|
||||
"""
|
||||
Get a list of problems that the student has attempted from ORA.
|
||||
Add in metadata as needed.
|
||||
"""
|
||||
def __init__(self, course_id, user_id):
|
||||
"""
|
||||
@param course_id: The id of a course object. Get using course.id.
|
||||
@param user_id: The anonymous id of the user, from the unique_id_for_user function.
|
||||
"""
|
||||
self.course_id = course_id
|
||||
self.user_id = user_id
|
||||
|
||||
# We want to append this string to all of our error messages.
|
||||
self.course_error_ending = _("for course {0} and student {1}.").format(self.course_id, user_id)
|
||||
|
||||
# This is our generic error message.
|
||||
self.error_text = STUDENT_ERROR_MESSAGE
|
||||
self.success = False
|
||||
|
||||
# Create a service to query edX ORA.
|
||||
self.controller_qs = create_controller_query_service()
|
||||
|
||||
def fetch_from_grading_service(self):
|
||||
"""
|
||||
Fetch a list of problems that the student has answered from ORA.
|
||||
Handle various error conditions.
|
||||
@return: A boolean success indicator.
|
||||
"""
|
||||
# In the case of multiple calls, ensure that success is false initially.
|
||||
self.success = False
|
||||
try:
|
||||
#Get list of all open ended problems that the grading server knows about
|
||||
problem_list_dict = self.controller_qs.get_grading_status_list(self.course_id, self.user_id)
|
||||
except GradingServiceError:
|
||||
log.error("Problem contacting open ended grading service " + self.course_error_ending)
|
||||
return self.success
|
||||
except ValueError:
|
||||
log.error("Problem with results from external grading service for open ended" + self.course_error_ending)
|
||||
return self.success
|
||||
|
||||
success = problem_list_dict['success']
|
||||
if 'error' in problem_list_dict:
|
||||
self.error_text = problem_list_dict['error']
|
||||
return success
|
||||
if 'problem_list' not in problem_list_dict:
|
||||
log.error("Did not receive a problem list in ORA response" + self.course_error_ending)
|
||||
return success
|
||||
|
||||
self.problem_list = problem_list_dict['problem_list']
|
||||
|
||||
self.success = True
|
||||
return self.success
|
||||
|
||||
def add_problem_data(self, base_course_url):
|
||||
"""
|
||||
Add metadata to problems.
|
||||
@param base_course_url: the base url for any course. Can get with reverse('course')
|
||||
@return: A list of valid problems in the course and their appended data.
|
||||
"""
|
||||
# Our list of valid problems.
|
||||
valid_problems = []
|
||||
|
||||
if not self.success or not isinstance(self.problem_list, list):
|
||||
log.error("Called add_problem_data without a valid problem list" + self.course_error_ending)
|
||||
return valid_problems
|
||||
|
||||
# Iterate through all of our problems and add data.
|
||||
for problem in self.problem_list:
|
||||
try:
|
||||
# Try to load the problem.
|
||||
usage_key = self.course_id.make_usage_key_from_deprecated_string(problem['location'])
|
||||
problem_url_parts = search.path_to_location(modulestore(), usage_key)
|
||||
except (ItemNotFoundError, NoPathToItem):
|
||||
# If the problem cannot be found at the location received from the grading controller server,
|
||||
# it has been deleted by the course author. We should not display it.
|
||||
error_message = "Could not find module for course {0} at location {1}".format(self.course_id,
|
||||
problem['location'])
|
||||
log.error(error_message)
|
||||
continue
|
||||
|
||||
# Get the problem url in the courseware.
|
||||
problem_url = generate_problem_url(problem_url_parts, base_course_url)
|
||||
|
||||
# Map the grader name from ORA to a human readable version.
|
||||
grader_type_display_name = GRADER_DISPLAY_NAMES.get(problem['grader_type'], "edX Assessment")
|
||||
problem['actual_url'] = problem_url
|
||||
problem['grader_type_display_name'] = grader_type_display_name
|
||||
valid_problems.append(problem)
|
||||
return valid_problems
|
||||
@@ -1,401 +0,0 @@
|
||||
import logging
|
||||
|
||||
from django.views.decorators.cache import cache_control
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.access import has_access
|
||||
from courseware.tabs import EnrolledTab
|
||||
|
||||
from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError
|
||||
import json
|
||||
from student.models import unique_id_for_user
|
||||
|
||||
from open_ended_grading import open_ended_notifications
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import search
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from xmodule.modulestore.exceptions import NoPathToItem
|
||||
|
||||
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from open_ended_grading.utils import (
|
||||
STAFF_ERROR_MESSAGE, StudentProblemList, generate_problem_url, create_controller_query_service
|
||||
)
|
||||
from xblock_django.models import XBlockDisableConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _reverse_with_slash(url_name, course_key):
|
||||
"""
|
||||
Reverses the URL given the name and the course id, and then adds a trailing slash if
|
||||
it does not exist yet.
|
||||
@param url_name: The name of the url (eg 'staff_grading').
|
||||
@param course_id: The id of the course object (eg course.id).
|
||||
@returns: The reversed url with a trailing slash.
|
||||
"""
|
||||
ajax_url = _reverse_without_slash(url_name, course_key)
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
return ajax_url
|
||||
|
||||
|
||||
def _reverse_without_slash(url_name, course_key):
|
||||
course_id = course_key.to_deprecated_string()
|
||||
ajax_url = reverse(url_name, kwargs={'course_id': course_id})
|
||||
return ajax_url
|
||||
|
||||
|
||||
DESCRIPTION_DICT = {
|
||||
'Peer Grading': _("View all problems that require peer assessment in this particular course."),
|
||||
'Staff Grading': _("View ungraded submissions submitted by students for the open ended problems in the course."),
|
||||
'Problems you have submitted': _("View open ended problems that you have previously submitted for grading."),
|
||||
'Flagged Submissions': _("View submissions that have been flagged by students as inappropriate."),
|
||||
}
|
||||
|
||||
ALERT_DICT = {
|
||||
'Peer Grading': _("New submissions to grade"),
|
||||
'Staff Grading': _("New submissions to grade"),
|
||||
'Problems you have submitted': _("New grades have been returned"),
|
||||
'Flagged Submissions': _("Submissions have been flagged for review"),
|
||||
}
|
||||
|
||||
|
||||
class StaffGradingTab(EnrolledTab):
|
||||
"""
|
||||
A tab for staff grading.
|
||||
"""
|
||||
type = 'staff_grading'
|
||||
title = _("Staff grading")
|
||||
view_name = "staff_grading"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
|
||||
return False
|
||||
if user and not has_access(user, 'staff', course, course.id):
|
||||
return False
|
||||
return "combinedopenended" in course.advanced_modules
|
||||
|
||||
|
||||
class PeerGradingTab(EnrolledTab):
|
||||
"""
|
||||
A tab for peer grading.
|
||||
"""
|
||||
type = 'peer_grading'
|
||||
# Translators: "Peer grading" appears on a tab that allows
|
||||
# students to view open-ended problems that require grading
|
||||
title = _("Peer grading")
|
||||
view_name = "peer_grading"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
|
||||
return False
|
||||
if not super(PeerGradingTab, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return "combinedopenended" in course.advanced_modules
|
||||
|
||||
|
||||
class OpenEndedGradingTab(EnrolledTab):
|
||||
"""
|
||||
A tab for open ended grading.
|
||||
"""
|
||||
type = 'open_ended'
|
||||
# Translators: "Open Ended Panel" appears on a tab that, when clicked, opens up a panel that
|
||||
# displays information about open-ended problems that a user has submitted or needs to grade
|
||||
title = _("Open Ended Panel")
|
||||
view_name = "open_ended_notifications"
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls, course, user=None):
|
||||
if XBlockDisableConfig.is_block_type_disabled('combinedopenended'):
|
||||
return False
|
||||
if not super(OpenEndedGradingTab, cls).is_enabled(course, user=user):
|
||||
return False
|
||||
return "combinedopenended" in course.advanced_modules
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def staff_grading(request, course_id):
|
||||
"""
|
||||
Show the instructor grading interface.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
ajax_url = _reverse_with_slash('staff_grading', course_key)
|
||||
|
||||
return render_to_response('instructor/staff_grading.html', {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
# Checked above
|
||||
'staff_access': True, })
|
||||
|
||||
|
||||
def find_peer_grading_module(course):
|
||||
"""
|
||||
Given a course, finds the first peer grading module in it.
|
||||
@param course: A course object.
|
||||
@return: boolean found_module, string problem_url
|
||||
"""
|
||||
|
||||
# Reverse the base course url.
|
||||
base_course_url = reverse('courses')
|
||||
found_module = False
|
||||
problem_url = ""
|
||||
|
||||
# Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs.
|
||||
items = modulestore().get_items(course.id, qualifiers={'category': 'peergrading'})
|
||||
# See if any of the modules are centralized modules (ie display info from multiple problems)
|
||||
items = [i for i in items if not getattr(i, "use_for_single_location", True)]
|
||||
# Loop through all potential peer grading modules, and find the first one that has a path to it.
|
||||
for item in items:
|
||||
# Generate a url for the first module and redirect the user to it.
|
||||
try:
|
||||
problem_url_parts = search.path_to_location(modulestore(), item.location)
|
||||
except NoPathToItem:
|
||||
# In the case of nopathtoitem, the peer grading module that was found is in an invalid state, and
|
||||
# can no longer be accessed. Log an informational message, but this will not impact normal behavior.
|
||||
log.info(u"Invalid peer grading module location %s in course %s. This module may need to be removed.", item.location, course.id)
|
||||
continue
|
||||
problem_url = generate_problem_url(problem_url_parts, base_course_url)
|
||||
found_module = True
|
||||
|
||||
return found_module, problem_url
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def peer_grading(request, course_id):
|
||||
'''
|
||||
When a student clicks on the "peer grading" button in the open ended interface, link them to a peer grading
|
||||
xmodule in the course.
|
||||
'''
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
#Get the current course
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
found_module, problem_url = find_peer_grading_module(course)
|
||||
if not found_module:
|
||||
error_message = _("""
|
||||
Error with initializing peer grading.
|
||||
There has not been a peer grading module created in the courseware that would allow you to grade others.
|
||||
Please check back later for this.
|
||||
""")
|
||||
log.exception(error_message + u"Current course is: {0}".format(course_id))
|
||||
return HttpResponse(error_message)
|
||||
|
||||
return HttpResponseRedirect(problem_url)
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def student_problem_list(request, course_id):
|
||||
"""
|
||||
Show a list of problems they have attempted to a student.
|
||||
Fetch the list from the grading controller server and append some data.
|
||||
@param request: The request object for this view.
|
||||
@param course_id: The id of the course to get the problem list for.
|
||||
@return: Renders an HTML problem list table.
|
||||
"""
|
||||
assert isinstance(course_id, basestring)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
# Load the course. Don't catch any errors here, as we want them to be loud.
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# The anonymous student id is needed for communication with ORA.
|
||||
student_id = unique_id_for_user(request.user)
|
||||
base_course_url = reverse('courses')
|
||||
error_text = ""
|
||||
|
||||
student_problem_list = StudentProblemList(course_key, student_id)
|
||||
# Get the problem list from ORA.
|
||||
success = student_problem_list.fetch_from_grading_service()
|
||||
# If we fetched the problem list properly, add in additional problem data.
|
||||
if success:
|
||||
# Add in links to problems.
|
||||
valid_problems = student_problem_list.add_problem_data(base_course_url)
|
||||
else:
|
||||
# Get an error message to show to the student.
|
||||
valid_problems = []
|
||||
error_text = student_problem_list.error_text
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_problems', course_key)
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'course_id': course_key.to_deprecated_string(),
|
||||
'ajax_url': ajax_url,
|
||||
'success': success,
|
||||
'problem_list': valid_problems,
|
||||
'error_text': error_text,
|
||||
# Checked above
|
||||
'staff_access': False,
|
||||
}
|
||||
|
||||
return render_to_response('open_ended_problems/open_ended_problems.html', context)
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def flagged_problem_list(request, course_id):
|
||||
'''
|
||||
Show a student problem list
|
||||
'''
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'staff', course_key)
|
||||
|
||||
# call problem list service
|
||||
success = False
|
||||
error_text = ""
|
||||
problem_list = []
|
||||
|
||||
# Make a service that can query edX ORA.
|
||||
controller_qs = create_controller_query_service()
|
||||
try:
|
||||
problem_list_dict = controller_qs.get_flagged_problem_list(course_key)
|
||||
success = problem_list_dict['success']
|
||||
if 'error' in problem_list_dict:
|
||||
error_text = problem_list_dict['error']
|
||||
problem_list = []
|
||||
else:
|
||||
problem_list = problem_list_dict['flagged_submissions']
|
||||
|
||||
except GradingServiceError:
|
||||
#This is a staff_facing_error
|
||||
error_text = STAFF_ERROR_MESSAGE
|
||||
#This is a dev_facing_error
|
||||
log.error("Could not get flagged problem list from external grading service for open ended.")
|
||||
success = False
|
||||
# catch error if if the json loads fails
|
||||
except ValueError:
|
||||
#This is a staff_facing_error
|
||||
error_text = STAFF_ERROR_MESSAGE
|
||||
#This is a dev_facing_error
|
||||
log.error("Could not parse problem list from external grading service response.")
|
||||
success = False
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_flagged_problems', course_key)
|
||||
context = {
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
'success': success,
|
||||
'problem_list': problem_list,
|
||||
'error_text': error_text,
|
||||
# Checked above
|
||||
'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)
|
||||
def combined_notifications(request, course_id):
|
||||
"""
|
||||
Gets combined notifications from the grading controller and displays them
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
user = request.user
|
||||
notifications = open_ended_notifications.combined_notifications(course, user)
|
||||
response = notifications['response']
|
||||
notification_tuples = open_ended_notifications.NOTIFICATION_TYPES
|
||||
|
||||
notification_list = []
|
||||
for response_num in xrange(len(notification_tuples)):
|
||||
tag = notification_tuples[response_num][0]
|
||||
if tag in response:
|
||||
url_name = notification_tuples[response_num][1]
|
||||
human_name = notification_tuples[response_num][2]
|
||||
url = _reverse_without_slash(url_name, course_key)
|
||||
has_img = response[tag]
|
||||
|
||||
# check to make sure we have descriptions and alert messages
|
||||
if human_name in DESCRIPTION_DICT:
|
||||
description = DESCRIPTION_DICT[human_name]
|
||||
else:
|
||||
description = ""
|
||||
|
||||
if human_name in ALERT_DICT:
|
||||
alert_message = ALERT_DICT[human_name]
|
||||
else:
|
||||
alert_message = ""
|
||||
|
||||
notification_item = {
|
||||
'url': url,
|
||||
'name': human_name,
|
||||
'alert': has_img,
|
||||
'description': description,
|
||||
'alert_message': alert_message
|
||||
}
|
||||
#The open ended panel will need to link the "peer grading" button in the panel to a peer grading
|
||||
#xmodule defined in the course. This checks to see if the human name of the server notification
|
||||
#that we are currently processing is "peer grading". If it is, it looks for a peer grading
|
||||
#module in the course. If none exists, it removes the peer grading item from the panel.
|
||||
if human_name == "Peer Grading":
|
||||
found_module, problem_url = find_peer_grading_module(course)
|
||||
if found_module:
|
||||
notification_list.append(notification_item)
|
||||
else:
|
||||
notification_list.append(notification_item)
|
||||
|
||||
ajax_url = _reverse_with_slash('open_ended_notifications', course_key)
|
||||
combined_dict = {
|
||||
'error_text': "",
|
||||
'notification_list': notification_list,
|
||||
'course': course,
|
||||
'success': True,
|
||||
'ajax_url': ajax_url,
|
||||
}
|
||||
|
||||
return render_to_response('open_ended_problems/combined_notifications.html', combined_dict)
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def take_action_on_flags(request, course_id):
|
||||
"""
|
||||
Takes action on student flagged submissions.
|
||||
Currently, only support unflag and ban actions.
|
||||
"""
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
|
||||
required = ['submission_id', 'action_type', 'student_id']
|
||||
for key in required:
|
||||
if key not in request.POST:
|
||||
error_message = u'Missing key {0} from submission. Please reload and try again.'.format(key)
|
||||
response = {
|
||||
'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE + error_message
|
||||
}
|
||||
return HttpResponse(json.dumps(response), content_type="application/json")
|
||||
|
||||
p = request.POST
|
||||
submission_id = p['submission_id']
|
||||
action_type = p['action_type']
|
||||
student_id = p['student_id']
|
||||
student_id = student_id.strip(' \t\n\r')
|
||||
submission_id = submission_id.strip(' \t\n\r')
|
||||
action_type = action_type.lower().strip(' \t\n\r')
|
||||
|
||||
# Make a service that can query edX ORA.
|
||||
controller_qs = create_controller_query_service()
|
||||
try:
|
||||
response = controller_qs.take_action_on_flags(course_key, student_id, submission_id, action_type)
|
||||
return HttpResponse(json.dumps(response), content_type="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception(
|
||||
u"Error taking action on flagged peer grading submissions, "
|
||||
u"submission_id: {0}, action_type: {1}, grader_id: {2}"
|
||||
.format(submission_id, action_type, student_id)
|
||||
)
|
||||
response = {
|
||||
'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE
|
||||
}
|
||||
return HttpResponse(json.dumps(response), content_type="application/json")
|
||||
@@ -1029,26 +1029,6 @@ PAID_COURSE_REGISTRATION_CURRENCY = ['usd', '$']
|
||||
# Members of this group are allowed to generate payment reports
|
||||
PAYMENT_REPORT_GENERATOR_GROUP = 'shoppingcart_report_access'
|
||||
|
||||
################################# open ended grading config #####################
|
||||
|
||||
#By setting up the default settings with an incorrect user name and password,
|
||||
# will get an error when attempting to connect
|
||||
OPEN_ENDED_GRADING_INTERFACE = {
|
||||
'url': 'http://example.com/peer_grading',
|
||||
'username': 'incorrect_user',
|
||||
'password': 'incorrect_pass',
|
||||
'staff_grading': 'staff_grading',
|
||||
'peer_grading': 'peer_grading',
|
||||
'grading_controller': 'grading_controller'
|
||||
}
|
||||
|
||||
# Used for testing, debugging peer grading
|
||||
MOCK_PEER_GRADING = False
|
||||
|
||||
# Used for testing, debugging staff grading
|
||||
MOCK_STAFF_GRADING = False
|
||||
|
||||
|
||||
################################# EdxNotes config #########################
|
||||
|
||||
# Configure the LMS to use our stub EdxNotes implementation
|
||||
@@ -1828,7 +1808,6 @@ INSTALLED_APPS = (
|
||||
'dashboard',
|
||||
'instructor',
|
||||
'instructor_task',
|
||||
'open_ended_grading',
|
||||
'openedx.core.djangoapps.course_groups',
|
||||
'bulk_email',
|
||||
'branding',
|
||||
|
||||
72
lms/urls.py
72
lms/urls.py
@@ -549,61 +549,6 @@ urlpatterns += (
|
||||
),
|
||||
# see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls
|
||||
|
||||
# Open Ended grading views
|
||||
url(
|
||||
r'^courses/{}/staff_grading$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.staff_grading',
|
||||
name='staff_grading',
|
||||
),
|
||||
url(
|
||||
r'^courses/{}/staff_grading/get_next$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.staff_grading_service.get_next',
|
||||
name='staff_grading_get_next',
|
||||
),
|
||||
url(
|
||||
r'^courses/{}/staff_grading/save_grade$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.staff_grading_service.save_grade',
|
||||
name='staff_grading_save_grade',
|
||||
),
|
||||
url(
|
||||
r'^courses/{}/staff_grading/get_problem_list$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.staff_grading_service.get_problem_list',
|
||||
name='staff_grading_get_problem_list',
|
||||
),
|
||||
|
||||
# Open Ended problem list
|
||||
url(
|
||||
r'^courses/{}/open_ended_problems$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.student_problem_list',
|
||||
name='open_ended_problems',
|
||||
),
|
||||
|
||||
# Open Ended flagged problem list
|
||||
url(
|
||||
r'^courses/{}/open_ended_flagged_problems$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.flagged_problem_list',
|
||||
name='open_ended_flagged_problems',
|
||||
),
|
||||
url(
|
||||
r'^courses/{}/open_ended_flagged_problems/take_action_on_flags$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.take_action_on_flags',
|
||||
name='open_ended_flagged_problems_take_action',
|
||||
),
|
||||
|
||||
# Cohorts management
|
||||
url(
|
||||
r'^courses/{}/cohorts/settings$'.format(
|
||||
@@ -655,23 +600,6 @@ urlpatterns += (
|
||||
name='cohort_discussion_topics',
|
||||
),
|
||||
|
||||
# Open Ended Notifications
|
||||
url(
|
||||
r'^courses/{}/open_ended_notifications$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.combined_notifications',
|
||||
name='open_ended_notifications',
|
||||
),
|
||||
|
||||
url(
|
||||
r'^courses/{}/peer_grading$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
),
|
||||
'open_ended_grading.views.peer_grading',
|
||||
name='peer_grading',
|
||||
),
|
||||
|
||||
url(
|
||||
r'^courses/{}/notes$'.format(
|
||||
settings.COURSE_ID_PATTERN,
|
||||
|
||||
Reference in New Issue
Block a user