Commit fb777de1 by Diana Huang

Merge pull request #1401 from MITx/feature/vik/peer-grading-flagging

Feature/vik/peer grading flagging
parents 4999a8e9 1dab4aba
......@@ -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'])
......@@ -194,11 +199,57 @@ def student_problem_list(request, course_id):
'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'
}
}
......
# 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
......
<%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$',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment