Merge pull request #1110 from MITx/diana/instructor-grading-styling

Updates to the instructor grading pages
This commit is contained in:
Victor Shnayder
2012-12-10 10:39:19 -08:00
6 changed files with 371 additions and 90 deletions

View File

@@ -29,16 +29,21 @@ class MockStaffGradingService(object):
def __init__(self):
self.cnt = 0
def get_next(self, course_id, grader_id):
def get_next(self,course_id, location, grader_id):
self.cnt += 1
return json.dumps({'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 save_grade(self, course_id, grader_id, submission_id, score, feedback):
return self.get_next(course_id, grader_id)
return self.get_next(course_id, 'fake location', grader_id)
class StaffGradingService(object):
@@ -53,6 +58,7 @@ class StaffGradingService(object):
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.session = requests.session()
@@ -96,13 +102,43 @@ class StaffGradingService(object):
return response
def get_problem_list(self, course_id, grader_id):
"""
Get the list of problems for a given course.
def get_next(self, course_id, grader_id):
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:
json string 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.
"""
op = lambda: self.session.get(self.get_problem_list_url,
allow_redirects = False,
params={'course_id': course_id,
'grader_id': grader_id})
try:
r = self._try_with_login(op)
except (RequestException, ConnectionError, HTTPError) as err:
# reraise as promised GradingServiceError, but preserve stacktrace.
raise GradingServiceError, str(err), sys.exc_info()[2]
return r.text
def get_next(self, course_id, location, grader_id):
"""
Get the next thing to grade.
Args:
course_id: course id to get submission for
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:
@@ -115,7 +151,7 @@ class StaffGradingService(object):
"""
op = lambda: self.session.get(self.get_next_url,
allow_redirects=False,
params={'course_id': course_id,
params={'location': location,
'grader_id': grader_id})
try:
r = self._try_with_login(op)
@@ -198,7 +234,8 @@ def _check_access(user, course_id):
def get_next(request, course_id):
"""
Get the next thing to grade for course_id.
Get the next thing to grade for course_id and with the location specified
in the .
Returns a json dict with the following keys:
@@ -218,17 +255,45 @@ def get_next(request, course_id):
"""
_check_access(request.user, course_id)
return HttpResponse(_get_next(course_id, request.user.id),
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 = request.user.id
p = request.POST
location = p['location']
return HttpResponse(_get_next(course_id, request.user.id, location),
mimetype="application/json")
def _get_next(course_id, grader_id):
def get_problem_list(request, course_id):
"""
Get all the problems for the given course id
TODO: fill in all of this stuff
"""
_check_access(request.user, course_id)
try:
response = grading_service().get_problem_list(course_id, request.user.id)
return HttpResponse(response,
mimetype="application/json")
except GradingServiceError:
log.exception("Error from grading service. server url: {0}"
.format(grading_service().url))
return HttpResponse(json.dumps({'success': False,
'error': 'Could not connect to grading service'}))
def _get_next(course_id, grader_id, location):
"""
Implementation of get_next (also called from save_grade) -- returns a json string
"""
try:
return grading_service().get_next(course_id, grader_id)
return grading_service().get_next(course_id, location, grader_id)
except GradingServiceError:
log.exception("Error from grading service. server url: {0}"
.format(grading_service().url))
@@ -255,15 +320,17 @@ def save_grade(request, course_id):
if request.method != 'POST':
raise Http404
required = set('score', 'feedback', 'submission_id')
required = set(['score', 'feedback', 'submission_id', 'location'])
actual = set(request.POST.keys())
log.debug(actual)
missing = required - actual
if len(missing) != 0:
if len(missing) > 0:
return _err_response('Missing required keys {0}'.format(
', '.join(missing)))
grader_id = request.user.id
p = request.POST
location = p['location']
try:
result_json = grading_service().save_grade(course_id,
@@ -286,6 +353,6 @@ def save_grade(request, course_id):
return _err_response('Grading service failed')
# Ok, save_grade seemed to work. Get the next submission to grade.
return HttpResponse(_get_next(course_id, grader_id),
return HttpResponse(_get_next(course_id, grader_id, location),
mimetype="application/json")

View File

@@ -236,6 +236,7 @@ class TestStaffGradingService(ct.PageLoader):
self.student = 'view@test.com'
self.instructor = 'view2@test.com'
self.password = 'foo'
self.location = 'TestLocation'
self.create_account('u1', self.student, self.password)
self.create_account('u2', self.instructor, self.password)
self.activate_user(self.student)
@@ -271,8 +272,9 @@ class TestStaffGradingService(ct.PageLoader):
self.login(self.instructor, self.password)
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
data = {'location': self.location}
r = self.check_for_get_code(200, url)
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success'])
self.assertEquals(d['submission_id'], self.mock_service.cnt)
@@ -285,7 +287,8 @@ class TestStaffGradingService(ct.PageLoader):
data = {'score': '12',
'feedback': 'great!',
'submission_id': '123'}
'submission_id': '123',
'location': self.location}
r = self.check_for_post_code(200, url, data)
d = json.loads(r.content)
self.assertTrue(d['success'], str(d))

View File

@@ -21,34 +21,72 @@ class StaffGradingBackend
# should take a location as an argument
if cmd == 'get_next'
@mock_cnt++
response =
success: true
problem_name: 'Problem 1'
num_left: 3
num_total: 5
prompt: 'This is a fake prompt'
submission: 'submission! ' + @mock_cnt
rubric: 'A rubric! ' + @mock_cnt
submission_id: @mock_cnt
max_score: 2 + @mock_cnt % 3
ml_error_info : 'ML accuracy info: ' + @mock_cnt
switch data.location
when 'i4x://MITx/3.091x/problem/open_ended_demo1'
response =
success: true
problem_name: 'Problem 1'
num_graded: 3
min_for_ml: 5
num_pending: 4
prompt: '''
<h2>S11E3: Metal Bands</h2>
<p>Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.</p>
<img width="480" src="/static/images/LSQimages/shaded_metal_bands.png"/>
<p>* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled? </p>
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
'''
submission: '''
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
'''
rubric: '''
<ul>
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
</ul>
<p>Please score your response according to how many of the above components you identified:</p>
'''
submission_id: @mock_cnt
max_score: 2 + @mock_cnt % 3
ml_error_info : 'ML accuracy info: ' + @mock_cnt
when 'i4x://MITx/3.091x/problem/open_ended_demo2'
response =
success: true
problem_name: 'Problem 2'
num_graded: 2
min_for_ml: 5
num_pending: 4
prompt: 'This is a fake second problem'
submission: 'This is the best submission ever! ' + @mock_cnt
rubric: 'I am a rubric for grading things! ' + @mock_cnt
submission_id: @mock_cnt
max_score: 2 + @mock_cnt % 3
ml_error_info : 'ML accuracy info: ' + @mock_cnt
else
response =
success: false
else if cmd == 'save_grade'
console.log("eval: #{data.score} pts, Feedback: #{data.feedback}")
response =
@mock('get_next', {})
# get_probblem_list
# sends in a course_id and a grader_id
# should get back a list of problem_ids, problem_names, num_left, num_total
@mock('get_next', {location: data.location})
# get_problem_list
# should get back a list of problem_ids, problem_names, num_graded, min_for_ml
else if cmd == 'get_problem_list'
response =
success: true
problem_list: [
{location: 'i4x://MITx/3.091x/problem/open_ended_demo', \
problem_name: "Problem 1", num_left: 3, num_total: 5},
{location: 'i4x://MITx/3.091x/problem/open_ended_demo', \
problem_name: "Problem 2", num_left: 1, num_total: 5}
]
@mock_cnt = 1
response =
success: true
problem_list: [
{location: 'i4x://MITx/3.091x/problem/open_ended_demo1', \
problem_name: "Problem 1", num_graded: 3, num_pending: 5, min_for_ml: 10},
{location: 'i4x://MITx/3.091x/problem/open_ended_demo2', \
problem_name: "Problem 2", num_graded: 1, num_pending: 5, min_for_ml: 10}
]
else
response =
success: false
@@ -81,9 +119,14 @@ class StaffGrading
@backend = backend
# all the jquery selectors
@problem_list_container = $('.problem-list-container')
@problem_list = $('.problem-list')
@error_container = $('.error-container')
@message_container = $('.message-container')
@prompt_name_container = $('.prompt-name')
@prompt_container = $('.prompt-container')
@prompt_wrapper = $('.prompt-wrapper')
@@ -92,11 +135,18 @@ class StaffGrading
@rubric_container = $('.rubric-container')
@rubric_wrapper = $('.rubric-wrapper')
@grading_wrapper = $('.grading-wrapper')
@feedback_area = $('.feedback-area')
@score_selection_container = $('.score-selection-container')
@submit_button = $('.submit-button')
@action_button = $('.action-button')
@problem_meta_info = $('.problem-meta-info-container')
@meta_info_wrapper = $('.meta-info-wrapper')
@ml_error_info_container = $('.ml-error-info-container')
@breadcrumbs = $('.breadcrumbs')
# model state
@state = state_no_data
@@ -108,17 +158,22 @@ class StaffGrading
@message = ''
@max_score = 0
@ml_error_info= ''
@location = ''
@prompt_name = ''
@min_for_ml = 0
@num_graded = 0
@num_pending = 0
@score = null
@problems = null
# action handlers
@submit_button.click @submit
# render intial state
@render_view()
# TODO: fix this to do something more intelligent
@action_button.click @submit
# send initial request automatically
@get_next_submission()
@get_problem_list()
setup_score_selection: =>
@@ -140,11 +195,12 @@ class StaffGrading
set_button_text: (text) =>
@submit_button.attr('value', text)
@action_button.attr('value', text)
graded_callback: (event) =>
@score = event.target.value
@state = state_graded
@message = ''
@render_view()
ajax_callback: (response) =>
@@ -153,8 +209,10 @@ class StaffGrading
@message = ''
if response.success
if response.submission
@data_loaded(response.prompt, response.submission, response.rubric, response.submission_id, response.max_score, response.ml_error_info)
if response.problem_list
@problems = response.problem_list
else if response.submission
@data_loaded(response)
else
@no_more(response.message)
else
@@ -162,14 +220,21 @@ class StaffGrading
@render_view()
get_next_submission: () ->
@backend.post('get_next', {}, @ajax_callback)
get_next_submission: (location) ->
@location = location
@list_view = false
@backend.post('get_next', {location: location}, @ajax_callback)
get_problem_list: () ->
@list_view = true
@backend.post('get_problem_list', {}, @ajax_callback)
submit_and_get_next: () ->
data =
score: @score
feedback: @feedback_area.val()
submission_id: @submission_id
location: @location
@backend.post('save_grade', data, @ajax_callback)
@@ -177,21 +242,28 @@ class StaffGrading
@error_msg = msg
@state = state_error
data_loaded: (prompt, submission, rubric, submission_id, max_score, ml_error_info) ->
@prompt = prompt
@submission = submission
@rubric = rubric
@submission_id = submission_id
data_loaded: (response) ->
@prompt = response.prompt
@submission = response.submission
@rubric = response.rubric
@submission_id = response.submission_id
@feedback_area.val('')
@max_score = max_score
@max_score = response.max_score
@score = null
@ml_error_info=ml_error_info
@ml_error_info=response.ml_error_info
@prompt_name = response.problem_name
@num_graded = response.num_graded
@min_for_ml = response.min_for_ml
@num_pending = response.num_pending
@state = state_grading
if not @max_score?
@error("No max score specified for submission.")
no_more: (message) ->
@prompt = null
@prompt_name = ''
@num_graded = 0
@min_for_ml = 0
@submission = null
@rubric = null
@ml_error_info = null
@@ -201,35 +273,92 @@ class StaffGrading
@max_score = 0
@state = state_no_data
render_view: () ->
# make the view elements match the state. Idempotent.
show_grading_elements = false
show_submit_button = true
@message_container.html(@message)
render_view: () ->
# clear the problem list and breadcrumbs
@problem_list.html('')
@breadcrumbs.html('')
@problem_list_container.toggle(@list_view)
if @backend.mock_backend
@message_container.append("<p>NOTE: Mocking backend.</p>")
@message = @message + "<p>NOTE: Mocking backend.</p>"
@message_container.html(@message)
@error_container.html(@error_msg)
@message_container.toggle(@message != "")
@error_container.toggle(@error_msg != "")
# only show the grading elements when we are not in list view or the state
# is invalid
show_grading_elements = !(@list_view || @state == state_error ||
@state == state_no_data)
@prompt_wrapper.toggle(show_grading_elements)
@submission_wrapper.toggle(show_grading_elements)
@rubric_wrapper.toggle(show_grading_elements)
@grading_wrapper.toggle(show_grading_elements)
@meta_info_wrapper.toggle(show_grading_elements)
@action_button.hide()
if @list_view
@render_list()
else
@render_problem()
problem_link:(problem) ->
link = $('<a>').attr('href', "javascript:void(0)").append(
"#{problem.problem_name} (#{problem.num_graded} graded, #{problem.num_pending} pending)")
.click =>
@get_next_submission problem.location
make_paragraphs: (text) ->
paragraph_split = text.split(/\n\s*\n/)
new_text = ''
for paragraph in paragraph_split
new_text += "<p>#{paragraph}</p>"
return new_text
render_list: () ->
for problem in @problems
@problem_list.append($('<li>').append(@problem_link(problem)))
render_problem: () ->
# make the view elements match the state. Idempotent.
show_submit_button = true
show_action_button = true
problem_list_link = $('<a>').attr('href', 'javascript:void(0);')
.append("< Back to problem list")
.click => @get_problem_list()
# set up the breadcrumbing
@breadcrumbs.append(problem_list_link)
if @state == state_error
@set_button_text('Try loading again')
show_action_button = true
else if @state == state_grading
@ml_error_info_container.html(@ml_error_info)
meta_list = $("<ul>")
meta_list.append("<li><span class='meta-info'>Pending - </span> #{@num_pending}</li>")
meta_list.append("<li><span class='meta-info'>Graded - </span> #{@num_graded}</li>")
meta_list.append("<li><span class='meta-info'>Needed for ML - </span> #{Math.max(@min_for_ml - @num_graded)}</li>")
@problem_meta_info.html(meta_list)
@prompt_container.html(@prompt)
@submission_container.html(@submission)
@prompt_name_container.html("#{@prompt_name}")
@submission_container.html(@make_paragraphs(@submission))
@rubric_container.html(@rubric)
show_grading_elements = true
# no submit button until user picks grade.
show_submit_button = false
show_action_button = false
@setup_score_selection()
else if @state == state_graded
show_grading_elements = true
@set_button_text('Submit')
show_action_button = false
else if @state == state_no_data
@message_container.html(@message)
@@ -239,21 +368,17 @@ class StaffGrading
@error('System got into invalid state ' + @state)
@submit_button.toggle(show_submit_button)
@prompt_wrapper.toggle(show_grading_elements)
@submission_wrapper.toggle(show_grading_elements)
@rubric_wrapper.toggle(show_grading_elements)
@ml_error_info_container.toggle(show_grading_elements)
@action_button.toggle(show_action_button)
submit: (event) =>
event.preventDefault()
if @state == state_error
@get_next_submission()
@get_next_submission(@location)
else if @state == state_graded
@submit_and_get_next()
else if @state == state_no_data
@get_next_submission()
@get_next_submission(@location)
else
@error('System got into invalid state for submission: ' + @state)

View File

@@ -1,6 +1,6 @@
div.staff-grading {
textarea.feedback-area {
height: 100px;
height: 75px;
margin: 20px;
}
@@ -26,4 +26,61 @@ div.staff-grading {
input[name='score-selection'] {
display: none;
}
ul
{
li
{
margin: 16px 0px;
}
}
.prompt-information-container,
.submission-wrapper,
.rubric-wrapper,
.grading-container
{
border: 1px solid gray;
padding: 15px;
}
.error-container
{
background-color: #FFCCCC;
padding: 15px;
margin-left: 0px;
}
.meta-info-wrapper
{
background-color: #eee;
padding:15px;
h3
{
font-size:1em;
}
ul
{
list-style-type: none;
font-size: .85em;
li
{
margin: 5px 0px;
}
}
}
.message-container
{
background-color: $yellow;
padding: 10px;
margin-left:0px;
}
.breadcrumbs
{
margin-top:20px;
margin-left:0px;
margin-bottom:5px;
font-size: .8em;
}
padding: 40px;
}

View File

@@ -18,42 +18,66 @@
<div class="staff-grading" data-ajax_url="${ajax_url}">
<h1>Staff grading</h1>
<div class="breadcrumbs">
</div>
<div class="error-container">
</div>
<div class="message-container">
</div>
<div class="ml-error-info-container">
<section class="problem-list-container">
<h2>Instructions</h2>
<div class="instructions">
<p>This is the list of problems that current need to be graded in order to train the machine learning models. Each problem needs to be trained separately, and we have indicated the number of student submissions that need to be graded in order for a model to be generated. You can grade more than the minimum required number of submissions--this will improve the accuracy of machine learning, though with diminishing returns. You can see the current accuracy of machine learning while grading.</p>
</div>
<h2>Problem List</h2>
<ul class="problem-list">
</ul>
</section>
<section class="prompt-wrapper">
<h3>Question prompt</h3>
<h2 class="prompt-name"></h2>
<div class="meta-info-wrapper">
<h3>Problem Information</h3>
<div class="problem-meta-info-container">
</div>
<h3>Maching Learning Information</h3>
<div class="ml-error-info-container">
</div>
</div>
<div class="prompt-information-container">
<h3>Question</h3>
<div class="prompt-container">
</div>
</section>
<section class="submission-wrapper">
<h3>Submission</h3>
<div class="submission-container">
</div>
</section>
<section class="rubric-wrapper">
<h3>Rubric</h3>
<div class="rubric-wrapper">
<h3>Grading Rubric</h3>
<div class="rubric-container">
</div>
</div>
<div class="submission-wrapper">
<h3>Student Submission</h3>
<div class="submission-container">
</div>
</div>
</section>
<div class="action-button">
<input type=button value="Submit" class="action-button" name="show" />
</div>
<section class="grading-wrapper">
<h2>Grading</h2>
<div class="grading-container">
<div class="evaluation">
<textarea name="feedback" placeholder="Feedback for student..."
class="feedback-area" cols="70" rows="10"></textarea>
<p class="score-selection-container">
</p>
<textarea name="feedback" placeholder="Feedback for student (optional)"
class="feedback-area" cols="70" ></textarea>
</div>
</section>
<div class="submission">
<input type="button" value="Submit" class="submit-button" name="show"/>
@@ -61,4 +85,5 @@
</div>
</div>
</section>

View File

@@ -243,6 +243,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.staff_grading_service.get_next', name='staff_grading_get_next'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/save_grade$',
'instructor.staff_grading_service.save_grade', name='staff_grading_save_grade'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/staff_grading/get_problem_list$',
'instructor.staff_grading_service.get_problem_list', name='staff_grading_get_problem_list'),
)
# discussion forums live within courseware, so courseware must be enabled first