Skip to content

Commit

Permalink
Merge pull request #768 from jhamrick/extra_credit
Browse files Browse the repository at this point in the history
Extra credit
  • Loading branch information
jhamrick committed Jun 2, 2017
2 parents 808e7cb + f96e6cb commit 100550a
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 29 deletions.
5 changes: 2 additions & 3 deletions nbgrader/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
from nbgrader.api import Base
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand Down
24 changes: 24 additions & 0 deletions nbgrader/alembic/versions/724cde206c17_add_extra_credit_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add extra credit column
Revision ID: 724cde206c17
Revises: 50a4d84c131a
Create Date: 2017-06-02 13:05:22.347671
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '724cde206c17'
down_revision = '50a4d84c131a'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('grade', sa.Column('extra_credit', sa.Float(), nullable=True))


def downgrade():
op.drop_column('grade', 'extra_credit')
8 changes: 6 additions & 2 deletions nbgrader/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,9 @@ class Grade(Base):
#: Score assigned by a human grader
manual_score = Column(Float())

#: Extra credit assigned by a human grader
extra_credit = Column(Float())

#: Whether a score needs to be assigned manually. This is True by default.
needs_manual_grade = Column(Boolean, default=True, nullable=False)

Expand All @@ -682,8 +685,8 @@ class Grade(Base):
#: for the score.
score = column_property(case(
[
(manual_score != None, manual_score),
(auto_score != None, auto_score)
(manual_score != None, manual_score + case([(extra_credit != None, extra_credit)], else_=literal_column("0.0"))),
(auto_score != None, auto_score + case([(extra_credit != None, extra_credit)], else_=literal_column("0.0")))
],
else_=literal_column("0.0")
))
Expand Down Expand Up @@ -713,6 +716,7 @@ def to_dict(self):
"student": self.student.id,
"auto_score": self.auto_score,
"manual_score": self.manual_score,
"extra_credit": self.extra_credit,
"max_score": self.max_score,
"needs_manual_grade": self.needs_manual_grade,
"failed_tests": self.failed_tests,
Expand Down
4 changes: 4 additions & 0 deletions nbgrader/docs/source/api/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Master version of an assignment

.. autoattribute:: needs_manual_grade

.. autoattribute:: kernelspec

.. automethod:: to_dict

.. autoclass:: GradeCell
Expand Down Expand Up @@ -257,6 +259,8 @@ Submitted assignments

.. autoattribute:: manual_score

.. autoattribute:: extra_credit

.. autoattribute:: score

.. autoattribute:: max_score
Expand Down
1 change: 1 addition & 0 deletions nbgrader/docs/source/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ JUPYTER
Jupyter
jupyter
jupyterhub
kernelspec
lgpage
linkcheck
linux
Expand Down
1 change: 1 addition & 0 deletions nbgrader/server_extensions/formgrader/apihandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def put(self, grade_id):

data = self.get_json_body()
grade.manual_score = data.get("manual_score", None)
grade.extra_credit = data.get("extra_credit", None)
if grade.manual_score is None and grade.auto_score is None:
grade.needs_manual_grade = True
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ div.nbgrader_cell .input_area {
border: none;
}

.score {
.score, .extra-credit {
color: black;
height: 29px;
margin-left: 1em;
Expand Down
38 changes: 28 additions & 10 deletions nbgrader/server_extensions/formgrader/static/js/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var GradeUI = Backbone.View.extend({

events: {
"change .score": "save",
"change .extra-credit": "save",
"click .full-credit": "assignFullCredit",
"click .no-credit": "assignNoCredit",
"click .mark-graded": "save"
Expand All @@ -10,18 +11,21 @@ var GradeUI = Backbone.View.extend({
initialize: function () {
this.$glyph = this.$el.find(".score-saved");
this.$score = this.$el.find(".score");
this.$extra_credit = this.$el.find(".extra-credit");
this.$mark_graded = this.$el.find(".mark-graded");

this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "request", this.animateSaving);
this.listenTo(this.model, "sync", this.animateSaved);

this.$score.attr("placeholder", this.model.get("auto_score"));
this.$extra_credit.attr("placeholder", 0.0);
this.render();
},

render: function () {
this.$score.val(this.model.get("manual_score"));
this.$extra_credit.val(this.model.get("extra_credit"));
if (this.model.get("needs_manual_grade")) {
this.$score.addClass("needs_manual_grade");
if (this.model.get("manual_score") !== null) {
Expand All @@ -34,22 +38,36 @@ var GradeUI = Backbone.View.extend({
},

save: function () {
var score, extra_credit;
if (this.$score.val() === "") {
this.model.save({"manual_score": null});
score = null;
} else {
var val = this.$score.val();
var max_score = this.model.get("max_score");
if (val > max_score) {
this.animateInvalidValue();
this.model.save({"manual_score": max_score});
this.animateInvalidValue(this.$score);
score = max_score;
} else if (val < 0) {
this.animateInvalidValue();
this.model.save({"manual_score": 0});
this.animateInvalidValue(this.$score);
score = 0;
} else {
this.model.save({"manual_score": val});
score = val;
}
}

if (this.$extra_credit.val() == "") {
extra_credit = null;
} else {
var val = this.$extra_credit.val();
if (val < 0) {
this.animateInvalidValue(this.$extra_credit);
extra_credit = 0;
} else {
extra_credit = val;
}
}

this.model.save({"manual_score": score, "extra_credit": extra_credit});
this.render();
},

Expand All @@ -69,14 +87,14 @@ var GradeUI = Backbone.View.extend({
$(document).trigger("finished_saving");
},

animateInvalidValue: function () {
animateInvalidValue: function (elem) {
var that = this;
this.$score.animate({
elem.animate({
"background-color": "#FF8888",
"border-color": "red"
}, 100, undefined, function () {
setTimeout(function () {
that.$score.animate({
elem.animate({
"background-color": "white",
"border-color": "white"
}, 100);
Expand All @@ -91,7 +109,7 @@ var GradeUI = Backbone.View.extend({
},

assignNoCredit: function () {
this.model.save({"manual_score": 0});
this.model.save({"manual_score": 0, "extra_credit": 0});
this.$score.select();
this.$score.focus();
}
Expand Down
3 changes: 3 additions & 0 deletions nbgrader/server_extensions/formgrader/templates/formgrade.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ window.MathJax = {
<span>
<input class="score tabbable" id="{{ cell.metadata.nbgrader.grade_id }}" style="width: 4em;" type="number" /> / {{ cell.metadata.nbgrader.points | float | round(2) }}
</span>
<span style="margin-left: 1em;">
+ <input class="extra-credit tabbable" id="{{ cell.metadata.nbgrader.grade_id }}_extra_credit" style="width: 3em;" type="number" /> (extra credit)
</span>
</div>
{%- endmacro %}

Expand Down
46 changes: 45 additions & 1 deletion nbgrader/tests/api/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,49 @@ def test_query_code_score_manualgraded(submissions):
assert [1.5, 3] == sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.code_score).all())
assert [1.5, 3] == sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.code_score).all())

def test_query_auto_score_extra_credit(submissions):
db, grades, _ = submissions

grades[0].auto_score = 10
grades[1].auto_score = 0
grades[2].auto_score = 5
grades[3].auto_score = 2.5

grades[0].extra_credit = 0.5
grades[1].extra_credit = 0
grades[2].extra_credit = 2.3
grades[3].extra_credit = 1.1
db.commit()

assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [0, 3.6, 7.3, 10.5]
assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [10.5, 10.9]
assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [10.5, 10.9]
assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [10.5, 10.9]

def test_query_manual_score_extra_credit(submissions):
db, grades, _ = submissions

grades[0].auto_score = 10
grades[1].auto_score = 0
grades[2].auto_score = 5
grades[3].auto_score = 2.5

grades[0].manual_score = 4
grades[1].manual_score = 1.5
grades[2].manual_score = 9
grades[3].manual_score = 3

grades[0].extra_credit = 0.5
grades[1].extra_credit = 0
grades[2].extra_credit = 2.3
grades[3].extra_credit = 1.1
db.commit()

assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [1.5, 4.1, 4.5, 11.3]
assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [6, 15.4]
assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [6, 15.4]
assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [6, 15.4]

def test_query_num_submissions(submissions):
db = submissions[0]

Expand Down Expand Up @@ -1187,7 +1230,7 @@ def test_grade_to_dict(submissions):
assert set(gd.keys()) == {
'id', 'name', 'notebook', 'assignment', 'student', 'auto_score',
'manual_score', 'max_score', 'needs_manual_grade', 'failed_tests',
'cell_type'}
'cell_type', 'extra_credit'}

assert gd['id'] == g.id
assert gd['name'] == g.name
Expand All @@ -1196,6 +1239,7 @@ def test_grade_to_dict(submissions):
assert gd['student'] == g.student.id
assert gd['auto_score'] is None
assert gd['manual_score'] is None
assert gd['extra_credit'] is None
assert gd['needs_manual_grade']
assert not gd['failed_tests']
assert gd['cell_type'] == g.cell_type
Expand Down
4 changes: 4 additions & 0 deletions nbgrader/tests/nbextensions/formgrade_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ def _get_score_box(browser, index):
return browser.find_elements_by_css_selector(".score")[index]


def _get_extra_credit_box(browser, index):
return browser.find_elements_by_css_selector(".extra-credit")[index]


def _save_comment(browser, index):
_send_keys_to_body(browser, Keys.ESCAPE)
glyph = browser.find_elements_by_css_selector(".comment-saved")[index]
Expand Down
Loading

0 comments on commit 100550a

Please sign in to comment.