diff --git a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
index 41896e6173..38fd6ba01c 100644
--- a/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
+++ b/common/lib/xmodule/xmodule/css/combinedopenended/display.scss
@@ -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;
}
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
index 52fd4c2547..cd85d93381 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -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 = "#{answer_val}"
+ new_text = "
#{answer_val}
"
@answer_area.replaceWith(new_text)
# wrap this so that it can be mocked
diff --git a/lms/djangoapps/open_ended_grading/controller_query_service.py b/lms/djangoapps/open_ended_grading/controller_query_service.py
index 7d515e2475..d40c9b4428 100644
--- a/lms/djangoapps/open_ended_grading/controller_query_service.py
+++ b/lms/djangoapps/open_ended_grading/controller_query_service.py
@@ -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
+
diff --git a/lms/djangoapps/open_ended_grading/open_ended_notifications.py b/lms/djangoapps/open_ended_grading/open_ended_notifications.py
index 43259f3e1b..fec893894f 100644
--- a/lms/djangoapps/open_ended_grading/open_ended_notifications.py
+++ b/lms/djangoapps/open_ended_grading/open_ended_notifications.py
@@ -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):
diff --git a/lms/djangoapps/open_ended_grading/peer_grading_service.py b/lms/djangoapps/open_ended_grading/peer_grading_service.py
index 2e31dd02e0..23e1488d9b 100644
--- a/lms/djangoapps/open_ended_grading/peer_grading_service.py
+++ b/lms/djangoapps/open_ended_grading/peer_grading_service.py
@@ -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},
diff --git a/lms/djangoapps/open_ended_grading/tests.py b/lms/djangoapps/open_ended_grading/tests.py
index 57ea4f319c..131fe5ad9f 100644
--- a/lms/djangoapps/open_ended_grading/tests.py
+++ b/lms/djangoapps/open_ended_grading/tests.py
@@ -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'])
diff --git a/lms/djangoapps/open_ended_grading/views.py b/lms/djangoapps/open_ended_grading/views.py
index 2ebd8778e6..156bdadddd 100644
--- a/lms/djangoapps/open_ended_grading/views.py
+++ b/lms/djangoapps/open_ended_grading/views.py
@@ -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')
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 0b76c6b241..172cf9bebb 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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'
}
}
diff --git a/lms/static/coffee/src/open_ended/open_ended.coffee b/lms/static/coffee/src/open_ended/open_ended.coffee
new file mode 100644
index 0000000000..cc8bad5473
--- /dev/null
+++ b/lms/static/coffee/src/open_ended/open_ended.coffee
@@ -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 = "" + msg + "
"
+ $('.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))
diff --git a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
index c4b87eb30e..ab16b34d12 100644
--- a/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
+++ b/lms/static/coffee/src/peer_grading/peer_grading_problem.coffee
@@ -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
diff --git a/lms/templates/open_ended_problems/open_ended_flagged_problems.html b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
new file mode 100644
index 0000000000..b4c6f43685
--- /dev/null
+++ b/lms/templates/open_ended_problems/open_ended_flagged_problems.html
@@ -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">${course.number} Flagged Open Ended Problems%block>
+
+<%include file="/courseware/course_navigation.html" args="active_page='open_ended_flagged_problems'" />
+
+<%block name="js_extra">
+ <%static:js group='open_ended'/>
+%block>
+
+
+
+
${error_text}
+
Flagged Open Ended Problems
+
Instructions
+
Here are a list of open ended problems for this course that have been flagged by students as potentially inappropriate.
+ % if success:
+ % if len(problem_list) == 0:
+
+ No flagged problems exist.
+
+ %else:
+
+
+ | Name |
+ Response |
+ |
+ |
+
+ %for problem in problem_list:
+
+ |
+ ${problem['problem_name']}
+ |
+
+ ${problem['student_response']}
+ |
+
+ Unflag
+ |
+
+ Ban
+ |
+
+
+ |
+
+ %endfor
+
+ %endif
+ %endif
+
+
diff --git a/lms/templates/peer_grading/peer_grading_problem.html b/lms/templates/peer_grading/peer_grading_problem.html
index cb9ed1c0fb..04ee7415ec 100644
--- a/lms/templates/peer_grading/peer_grading_problem.html
+++ b/lms/templates/peer_grading/peer_grading_problem.html
@@ -72,6 +72,7 @@
+ Flag this submission for review by course staff (use if the submission contains inappropriate content):
diff --git a/lms/urls.py b/lms/urls.py
index 2d4267ec71..7b7a70b6f2 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -288,6 +288,12 @@ if settings.COURSEWARE_ENABLED:
# Open Ended problem list
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_problems$',
'open_ended_grading.views.student_problem_list', name='open_ended_problems'),
+
+ # Open Ended flagged problem list
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/open_ended_flagged_problems$',
+ 'open_ended_grading.views.flagged_problem_list', name='open_ended_flagged_problems'),
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/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[^/]+/[^/]+/[^/]+)/cohorts$',