Merge pull request #1401 from MITx/feature/vik/peer-grading-flagging
Feature/vik/peer grading flagging
This commit is contained in:
@@ -442,12 +442,13 @@ section.open-ended-child {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
span.short-form-response {
|
||||
padding: 9px;
|
||||
div.short-form-response {
|
||||
background: #F6F6F6;
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0;
|
||||
margin-bottom: 20px;
|
||||
overflow-y: auto;
|
||||
height: 200px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ class @CombinedOpenEnded
|
||||
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
|
||||
if response.state == "done" or response.state=="post_assessment"
|
||||
delete window.queuePollerID
|
||||
@reload
|
||||
location.reload()
|
||||
else
|
||||
window.queuePollerID = window.setTimeout(@poll, 10000)
|
||||
|
||||
@@ -351,7 +351,7 @@ class @CombinedOpenEnded
|
||||
answer_id = @answer_area.attr('id')
|
||||
answer_val = @answer_area.val()
|
||||
new_text = ''
|
||||
new_text = "<span class='#{answer_class}' id='#{answer_id}'>#{answer_val}</span>"
|
||||
new_text = "<div class='#{answer_class}' id='#{answer_id}'>#{answer_val}</div>"
|
||||
@answer_area.replaceWith(new_text)
|
||||
|
||||
# wrap this so that it can be mocked
|
||||
|
||||
@@ -21,6 +21,8 @@ class ControllerQueryService(GradingService):
|
||||
self.is_unique_url = self.url + '/is_name_unique/'
|
||||
self.combined_notifications_url = self.url + '/combined_notifications/'
|
||||
self.grading_status_list_url = self.url + '/get_grading_status_list/'
|
||||
self.flagged_problem_list_url = self.url + '/get_flagged_problem_list/'
|
||||
self.take_action_on_flags_url = self.url + '/take_action_on_flags/'
|
||||
|
||||
def check_if_name_is_unique(self, location, problem_id, course_id):
|
||||
params = {
|
||||
@@ -57,3 +59,23 @@ class ControllerQueryService(GradingService):
|
||||
|
||||
response = self.get(self.grading_status_list_url, params)
|
||||
return response
|
||||
|
||||
def get_flagged_problem_list(self, course_id):
|
||||
params = {
|
||||
'course_id' : course_id,
|
||||
}
|
||||
|
||||
response = self.get(self.flagged_problem_list_url, params)
|
||||
return response
|
||||
|
||||
def take_action_on_flags(self, course_id, student_id, submission_id, action_type):
|
||||
params = {
|
||||
'course_id' : course_id,
|
||||
'student_id' : student_id,
|
||||
'submission_id' : submission_id,
|
||||
'action_type' : action_type
|
||||
}
|
||||
|
||||
response = self.post(self.take_action_on_flags_url, params)
|
||||
return response
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ 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')
|
||||
('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):
|
||||
|
||||
@@ -50,7 +50,7 @@ class MockPeerGradingService(object):
|
||||
'max_score': 4})
|
||||
|
||||
def save_grade(self, location, grader_id, submission_id,
|
||||
score, feedback, submission_key, rubric_scores):
|
||||
score, feedback, submission_key, rubric_scores, submission_flagged):
|
||||
return json.dumps({'success': True})
|
||||
|
||||
def is_student_calibrated(self, problem_location, grader_id):
|
||||
@@ -97,7 +97,7 @@ class PeerGradingService(GradingService):
|
||||
{'location': problem_location, 'grader_id': grader_id})
|
||||
return json.dumps(self._render_rubric(response))
|
||||
|
||||
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores):
|
||||
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,
|
||||
@@ -105,7 +105,8 @@ class PeerGradingService(GradingService):
|
||||
'submission_key': submission_key,
|
||||
'location': location,
|
||||
'rubric_scores': rubric_scores,
|
||||
'rubric_scores_complete': True}
|
||||
'rubric_scores_complete': True,
|
||||
'submission_flagged' : submission_flagged}
|
||||
return self.post(self.save_grade_url, data)
|
||||
|
||||
def is_student_calibrated(self, problem_location, grader_id):
|
||||
@@ -233,7 +234,7 @@ def save_grade(request, course_id):
|
||||
error: if there was an error in the submission, this is the error message
|
||||
"""
|
||||
_check_post(request)
|
||||
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
|
||||
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged'])
|
||||
success, message = _check_required(request, required)
|
||||
if not success:
|
||||
return _err_response(message)
|
||||
@@ -245,9 +246,10 @@ def save_grade(request, course_id):
|
||||
feedback = p['feedback']
|
||||
submission_key = p['submission_key']
|
||||
rubric_scores = p.getlist('rubric_scores[]')
|
||||
submission_flagged = p['submission_flagged']
|
||||
try:
|
||||
response = peer_grading_service().save_grade(location, grader_id, submission_id,
|
||||
score, feedback, submission_key, rubric_scores)
|
||||
score, feedback, submission_key, rubric_scores, submission_flagged)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
|
||||
|
||||
@@ -172,7 +172,8 @@ class TestPeerGradingService(ct.PageLoader):
|
||||
'submission_key': 'fake key',
|
||||
'score': '2',
|
||||
'feedback': 'This is feedback',
|
||||
'rubric_scores[]': [1, 2]}
|
||||
'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'])
|
||||
|
||||
@@ -25,6 +25,8 @@ import open_ended_notifications
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import search
|
||||
|
||||
from django.http import HttpResponse, Http404
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
@@ -54,12 +56,14 @@ def _reverse_without_slash(url_name, course_id):
|
||||
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."
|
||||
'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"
|
||||
'Problems you have submitted': "New grades have been returned",
|
||||
'Flagged Submissions' : "Submissions have been flagged for review"
|
||||
}
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def staff_grading(request, course_id):
|
||||
@@ -158,8 +162,9 @@ def student_problem_list(request, course_id):
|
||||
success = problem_list_dict['success']
|
||||
if 'error' in problem_list_dict:
|
||||
error_text = problem_list_dict['error']
|
||||
|
||||
problem_list = problem_list_dict['problem_list']
|
||||
problem_list = []
|
||||
else:
|
||||
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'])
|
||||
@@ -193,12 +198,58 @@ def student_problem_list(request, course_id):
|
||||
# Checked above
|
||||
'staff_access': False, })
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def flagged_problem_list(request, course_id):
|
||||
'''
|
||||
Show a student problem list
|
||||
'''
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
student_id = unique_id_for_user(request.user)
|
||||
|
||||
# call problem list service
|
||||
success = False
|
||||
error_text = ""
|
||||
problem_list = []
|
||||
base_course_url = reverse('courses')
|
||||
|
||||
try:
|
||||
problem_list_json = controller_qs.get_flagged_problem_list(course_id)
|
||||
problem_list_dict = json.loads(problem_list_json)
|
||||
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:
|
||||
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('open_ended_flagged_problems', course_id)
|
||||
|
||||
return render_to_response('open_ended_problems/open_ended_flagged_problems.html', {
|
||||
'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, })
|
||||
|
||||
@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 = get_course_with_access(request.user, course_id, 'load')
|
||||
user = request.user
|
||||
notifications = open_ended_notifications.combined_notifications(course, user)
|
||||
log.debug(notifications)
|
||||
response = notifications['response']
|
||||
notification_tuples=open_ended_notifications.NOTIFICATION_TYPES
|
||||
|
||||
@@ -243,5 +294,35 @@ def combined_notifications(request, course_id):
|
||||
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.
|
||||
"""
|
||||
if request.method != 'POST':
|
||||
raise Http404
|
||||
|
||||
|
||||
required = ['submission_id', 'action_type', 'student_id']
|
||||
for key in required:
|
||||
if key not in request.POST:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Missing key {0}'.format(key)}),
|
||||
mimetype="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')
|
||||
try:
|
||||
response = controller_qs.take_action_on_flags(course_id, student_id, submission_id, action_type)
|
||||
return HttpResponse(response, mimetype="application/json")
|
||||
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))
|
||||
return _err_response('Could not connect to grading service')
|
||||
|
||||
|
||||
|
||||
@@ -438,6 +438,7 @@ main_vendor_js = [
|
||||
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'))
|
||||
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'))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
@@ -468,7 +469,7 @@ PIPELINE_JS = {
|
||||
'source_filenames': sorted(
|
||||
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js + open_ended_js)
|
||||
) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
@@ -501,6 +502,10 @@ PIPELINE_JS = {
|
||||
'peer_grading' : {
|
||||
'source_filenames': peer_grading_js,
|
||||
'output_filename': 'js/peer_grading.js'
|
||||
},
|
||||
'open_ended' : {
|
||||
'source_filenames': open_ended_js,
|
||||
'output_filename': 'js/open_ended.js'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
65
lms/static/coffee/src/open_ended/open_ended.coffee
Normal file
65
lms/static/coffee/src/open_ended/open_ended.coffee
Normal file
@@ -0,0 +1,65 @@
|
||||
# This is a simple class that just hides the error container
|
||||
# and message container when they are empty
|
||||
# Can (and should be) expanded upon when our problem list
|
||||
# becomes more sophisticated
|
||||
class OpenEnded
|
||||
constructor: (ajax_url) ->
|
||||
@ajax_url = ajax_url
|
||||
@error_container = $('.error-container')
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
@problem_list = $('.problem-list')
|
||||
|
||||
@ban_button = $('.ban-button')
|
||||
@unflag_button = $('.unflag-button')
|
||||
@ban_button.click @ban
|
||||
@unflag_button.click @unflag
|
||||
|
||||
unflag: (event) =>
|
||||
event.preventDefault()
|
||||
parent_tr = $(event.target).parent().parent()
|
||||
tr_children = parent_tr.children()
|
||||
action_type = "unflag"
|
||||
submission_id = parent_tr.data('submission-id')
|
||||
student_id = parent_tr.data('student-id')
|
||||
callback_func = @after_action_wrapper($(event.target), action_type)
|
||||
@post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, callback_func)
|
||||
|
||||
ban: (event) =>
|
||||
event.preventDefault()
|
||||
parent_tr = $(event.target).parent().parent()
|
||||
tr_children = parent_tr.children()
|
||||
action_type = "ban"
|
||||
submission_id = parent_tr.data('submission-id')
|
||||
student_id = parent_tr.data('student-id')
|
||||
callback_func = @after_action_wrapper($(event.target), action_type)
|
||||
@post('take_action_on_flags', {'submission_id' : submission_id, 'student_id' : student_id, 'action_type' : action_type}, callback_func)
|
||||
|
||||
post: (cmd, data, callback) ->
|
||||
# if this post request fails, the error callback will catch it
|
||||
$.post(@ajax_url + cmd, data, callback)
|
||||
.error => callback({success: false, error: "Error occured while performing this operation"})
|
||||
|
||||
after_action_wrapper: (target, action_type) ->
|
||||
tr_parent = target.parent().parent()
|
||||
tr_children = tr_parent.children()
|
||||
action_taken = tr_children[4].firstElementChild
|
||||
action_taken.innerText = "#{action_type} done for student."
|
||||
return @handle_after_action
|
||||
|
||||
handle_after_action: (data) ->
|
||||
if !data.success
|
||||
@gentle_alert data.error
|
||||
|
||||
gentle_alert: (msg) =>
|
||||
if $('.message-container').length
|
||||
$('.message-container').remove()
|
||||
alert_elem = "<div class='message-container'>" + msg + "</div>"
|
||||
$('.error-container').after(alert_elem)
|
||||
$('.message-container').css(opacity: 0).animate(opacity: 1, 700)
|
||||
|
||||
ajax_url = $('.open-ended-problems').data('ajax_url')
|
||||
$(document).ready(() -> new OpenEnded(ajax_url))
|
||||
@@ -175,6 +175,7 @@ class PeerGradingProblem
|
||||
@submission_container = $('.submission-container')
|
||||
@prompt_container = $('.prompt-container')
|
||||
@rubric_container = $('.rubric-container')
|
||||
@flag_student_container = $('.flag-student-container')
|
||||
@calibration_panel = $('.calibration-panel')
|
||||
@grading_panel = $('.grading-panel')
|
||||
@content_panel = $('.content-panel')
|
||||
@@ -201,6 +202,7 @@ class PeerGradingProblem
|
||||
@action_button = $('.action-button')
|
||||
@calibration_feedback_button = $('.calibration-feedback-button')
|
||||
@interstitial_page_button = $('.interstitial-page-button')
|
||||
@flag_student_checkbox = $('.flag-checkbox')
|
||||
|
||||
Collapsible.setCollapsibles(@content_panel)
|
||||
|
||||
@@ -252,7 +254,8 @@ class PeerGradingProblem
|
||||
location: @location
|
||||
submission_id: @essay_id_input.val()
|
||||
submission_key: @submission_key_input.val()
|
||||
feedback: @feedback_area.val()
|
||||
feedback: @feedback_area.val()
|
||||
submission_flagged: @flag_student_checkbox.is(':checked')
|
||||
return data
|
||||
|
||||
|
||||
@@ -352,7 +355,7 @@ class PeerGradingProblem
|
||||
@grading_panel.find('.calibration-text').show()
|
||||
@calibration_panel.find('.grading-text').hide()
|
||||
@grading_panel.find('.grading-text').hide()
|
||||
|
||||
@flag_student_container.hide()
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_calibration_essay
|
||||
@@ -379,6 +382,7 @@ class PeerGradingProblem
|
||||
@grading_panel.find('.calibration-text').hide()
|
||||
@calibration_panel.find('.grading-text').show()
|
||||
@grading_panel.find('.grading-text').show()
|
||||
@flag_student_container.show()
|
||||
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_grade
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%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} Flagged Open Ended Problems</title></%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" />
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:js group='open_ended'/>
|
||||
</%block>
|
||||
|
||||
<section class="container">
|
||||
<div class="open-ended-problems" data-ajax_url="${ajax_url}">
|
||||
<div class="error-container">${error_text}</div>
|
||||
<h1>Flagged Open Ended Problems</h1>
|
||||
<h2>Instructions</h2>
|
||||
<p>Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.</p>
|
||||
% if success:
|
||||
% if len(problem_list) == 0:
|
||||
<div class="message-container">
|
||||
No flagged problems exist.
|
||||
</div>
|
||||
%else:
|
||||
<table class="problem-list">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Response</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
%for problem in problem_list:
|
||||
<tr data-submission-id="${problem['submission_id']}" data-student-id="${problem['student_id']}">
|
||||
<td>
|
||||
${problem['problem_name']}
|
||||
</td>
|
||||
<td>
|
||||
${problem['student_response']}
|
||||
</td>
|
||||
<td>
|
||||
<a href="#unflag" class="unflag-button action-button" data-action-type="unflag">Unflag</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#ban" class="ban-button action-button" data-action-type="ban">Ban</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-taken"></div>
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</table>
|
||||
%endif
|
||||
%endif
|
||||
</div>
|
||||
</section>
|
||||
@@ -72,6 +72,7 @@
|
||||
</p>
|
||||
<textarea name="feedback" placeholder="Feedback for student (optional)"
|
||||
class="feedback-area" cols="70" ></textarea>
|
||||
<p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -288,6 +288,12 @@ if settings.COURSEWARE_ENABLED:
|
||||
# Open Ended problem list
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_problems$',
|
||||
'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
|
||||
|
||||
# Open Ended flagged problem list
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
|
||||
'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems/take_action_on_flags$',
|
||||
'open_ended_grading.views.take_action_on_flags', name='open_ended_flagged_problems_take_action'),
|
||||
|
||||
# Cohorts management
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/cohorts$',
|
||||
|
||||
Reference in New Issue
Block a user