release new gradebook history

closes GRADE-82

Test plan:
* Make sure new gradebook history feature flag does not show up
* Make sure old gradebook history does not show up anywhere
* Make sure new gradebook history loads

Change-Id: I0cbf5f77900d079376dd1756073c237e25057526
Reviewed-on: https://gerrit.instructure.com/127138
Tested-by: Jenkins
Reviewed-by: Jeremy Neander <jneander@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
QA-Review: Anju Reddy <areddy@instructure.com>
Product-Review: Matt Goodwin <mattg@instructure.com>
This commit is contained in:
Neil Gupta 2017-09-22 17:29:09 -05:00
parent 3811dd1a24
commit 6a7acb384c
11 changed files with 41 additions and 627 deletions

View File

@ -409,24 +409,15 @@ class GradebooksController < ApplicationController
def history
if authorized_action(@context, @current_user, :manage_grades)
return new_history if @context.root_account.feature_enabled?(:new_gradebook_history)
crumbs.delete_if { |crumb| crumb[0] == "Grades" }
add_crumb(t("Gradebook History"),
context_url(@context, controller: :gradebooks, action: :history))
@page_title = t("Gradebook History")
@body_classes << "full-width padless-content"
js_bundle :gradebook_history
js_env({})
#
# Temporary disabling of this page for large courses
# We need some reworking of the gradebook history to allow using it
# in large courses in a performant manner. Until that happens, we're
# disabling it over a certain threshold.
#
submissions_count = @context.submissions.not_placeholder.count
submissions_limit = Setting.get('gradebook_history_submission_count_threshold', '0').to_i
if submissions_limit == 0 || submissions_count <= submissions_limit
# TODO this whole thing could go a LOT faster if you just got ALL the versions of ALL the submissions in this course then did a ruby sort_by day then grader
@days = SubmissionList.days(@context)
end
respond_to do |format|
format.html
end
render html: "", layout: true
end
end
@ -783,16 +774,6 @@ class GradebooksController < ApplicationController
end
end
def new_history
@page_title = t("Gradebook History")
@body_classes << "full-width padless-content"
js_bundle :react_gradebook_history
css_bundle :react_gradebook_history
js_env({})
render html: "", layout: true
end
def percentage(weight)
I18n.n(weight, percentage: true)
end

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2011 - present Instructure, Inc.
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
@ -16,7 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import $ from 'jquery'
import GradebookHistory from 'gradebook-history'
import React from 'react';
import ReactDOM from 'react-dom';
import GradebookHistoryApp from 'jsx/gradebook-history/GradebookHistoryApp';
$(document).ready(GradebookHistory.init)
ReactDOM.render(<GradebookHistoryApp />, document.getElementById('content'));

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import GradebookHistoryApp from 'jsx/gradebook-history/GradebookHistoryApp';
ReactDOM.render(<GradebookHistoryApp />, document.getElementById('content'));

View File

@ -1,103 +0,0 @@
/*
* Copyright (C) 2015 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "base/environment";
#gradebook-history {
margin: 0;
padding: 1em;
li {
list-style: none;
}
h4, .h4 {
margin: 0;
}
.assignment_header {
cursor: pointer;
display: flex;
align-items: center;
a {
padding-right: 5px;
float: left;
&:hover {
text-decoration: underline;
}
}
.changes {
@include fontSize(13px);
line-height: inherit;
min-width: 65px;
float: left;
text-align: center;
vertical-align: middle;
margin: $ic-sp/2;
border: none;
background: transparent;
color: $ic-font-color-dark;
text-shadow: none;
box-shadow: none;
&::after {
content: '|';
padding-left: $ic-sp/2;
}
}
.ui-icon {
float: left;
}
}
.assignment_details {
padding-bottom: $ic-sp/2;
table {
width: 100%;
text-align: center;
}
}
.revert-grade-link {
font-weight: normal;
padding: 0 3px;
line-height: 20px;
text-decoration: none;
.revert, .ui-icon {
opacity: 0;
}
.ui-icon {
display: inline;
padding: 0 0 0 18px;
}
.grade {
padding: 0 3px;
border-color: transparent;
}
&:hover, &:focus {
.revert, .ui-icon {
opacity: 1;
}
.grade {
border-color: inherit;
background-color: white;
}
}
}
.current_grade {
padding-left: 17px;
}
.loading {
background: #F5F8F9 url(/images/blue_small_loading.gif) no-repeat left center;
color: #bbb;
}
}

View File

@ -1,123 +0,0 @@
<%
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
%>
<% content_for :page_title do %><%= join_title t(:page_title, "Gradebook History"), @context.name %><% end %>
<% css_bundle :gradebook_history %>
<h1 class='screenreader-only'><%= t(:page_title, "Gradebook History") %></h1>
<% if !@days %>
<%# temorary disabling of this page for large contexts %>
<p>
<%= t 'temporary_disabled', "The gradebook history page is not currently available for a course of this size." %>
</p>
<% else %>
<a style="display: none;" title="POST" href="<%= context_url(@context, :update_submission_context_gradebook_url) %>" class="update_submission_grade_url">&nbsp;</a>
<ul id="gradebook-history">
<% @days.each do |day| %>
<li>
<h2 class='screenreader-only'><%= t(:grades_for_date, "Grades for Date") %></h2>
<h3><%= date_string(day.date) %></h3>
<ul>
<% day.graders.each do |grader| %>
<li class="grader">
<h4><%= grader.name %></h4>
<ul>
<% grader.assignments.each do |assignment| %>
<li class='gradebook-history-assignment'>
<div class="assignment_header">
<span class="changes ui-state-highlight ui-corner-all"><%= t(:submission_count, "change", :count => assignment.submission_count) %></span>
<a href="<%= context_url(@context, :context_assignment_url, assignment.assignment_id) %>" class="assignment-header" aria-controls="assignment_<%= assignment.assignment_id %>" aria-expanded="false">
<span aria-hidden="true"><%= assignment.name %></span>
<span class="screenreader-only"><%= t("%{assignment_name} grade history", :assignment_name => assignment.name) %></span>
</a>
<span class="ui-icon ui-icon-circle-arrow-s"></span>
<br class="clear" />
</div>
<div id="assignment_<%= assignment.assignment_id %>"class="assignment_details" style="display:none;">
<table class="ic-Table ic-Table--condensed ic-Table--hover-row ic-Table--striped">
<thead>
<tr>
<th><%= t('headers.student', "Student") %></th>
<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<%= t('headers.before', "Before") %>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<%= t('headers.after', "After") %>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th><%= t('headers.current', "Current") %></th>
</tr>
</thead>
<tbody>
<% assignment.submissions.each_with_index do |submission, i| %>
<tr data-assignment-id="<%= submission.assignment_id %>" data-user-id="<%= submission.user_id %>">
<th scope="row" class="ic-Table--header-row"><%= submission.student_name %></th>
<td>
<a
href="#"
class="ui-corner-all revert-grade-link"
data-grade="<%= display_grade(submission.previous_grade) %>"
>
<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>
<span
title="<%= graded_by_title(submission.previous_graded_at, submission.previous_grader) %>"
class='ui-corner-all grade'
>
<%= display_grade(format_grade(submission.previous_grade)) %>
</span>
<span class="revert"><%= t(:revert, "Revert to this grade") %></span>
</a>
</td>
<td>
<a
href="#"
class="ui-corner-all revert-grade-link"
data-grade="<%= display_grade(submission.new_grade) %>"
>
<span class="ui-icon ui-icon-arrowrefresh-1-w"></span>
<span
title="<%= graded_by_title(submission.new_graded_at, submission.new_grader) %>"
class='ui-corner-all grade'
>
<%= display_grade(format_grade(submission.new_grade)) %>
</span>
<span class="revert"><%= t(:revert, "Revert to this grade") %></span>
</a>
</td>
<td>
<span
title="<%= graded_by_title(submission.current_graded_at, submission.current_grader) %>"
class="current_grade <%= history_submission_class(submission) %>"
>
<%= display_grade(format_grade(submission.current_grade)) %>
</span>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</li>
<% end %>
</ul>
</li>
<% end %>
</ul>
</li>
<% end %>
</ul>
<% js_bundle :gradebook_history %>
<% end %>

View File

@ -0,0 +1,28 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class ClearNewGradebookHistoryFeatureFlags < ActiveRecord::Migration[5.0]
tag :postdeploy
def self.up
DataFixup::ClearFeatureFlags.run_async('new_gradebook_history')
end
def self.down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -507,15 +507,6 @@ END
state: "allowed",
beta: true
},
'new_gradebook_history' =>
{
display_name: -> { I18n.t('New Gradebook History') },
description: -> { I18n.t('Enable New Gradebook History page.') },
applies_to: "RootAccount",
state: "hidden",
beta: true,
development: true,
},
'new_user_tutorial' =>
{
display_name: -> { I18n.t('New User Tutorial')},

View File

@ -1,98 +0,0 @@
/*
* Copyright (C) 2011 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import I18n from 'i18n!gradebook'
import $ from 'jquery'
import GradeFormatHelper from 'jsx/gradebook/shared/helpers/GradeFormatHelper'
import './jquery.ajaxJSON'
import './jquery.instructure_date_and_time' /* datetimeString */
function announceUpdatedCurrentGrade (currentGrade) {
var noGrade = '--';
var flashMessage;
if (currentGrade === noGrade) {
flashMessage = I18n.t('Updated current grade to be empty');
} else {
flashMessage = I18n.t(
'Updated current grade to %{currentGrade}',
{ currentGrade: currentGrade }
);
}
$.screenReaderFlashMessage(flashMessage);
}
var GradebookHistory = {
init: function(){
$('.assignment_header').click(function(event) {
event.preventDefault();
var toggleLink = $(this).find('.assignment-header');
var currentState = toggleLink.attr('aria-expanded');
toggleLink.attr('aria-expanded', currentState == 'false' ? 'true' : 'false');
$(this).find('.ui-icon').toggleClass('ui-icon-circle-arrow-n').end()
.next('.assignment_details').slideToggle('fast');
});
$(".revert-grade-link").bind("mouseenter mouseleave", function(){
$(this).toggleClass("ui-state-hover");
})
.click(GradebookHistory.handleGradeSubmit);
},
handleGradeSubmit: function(event){
// 'this' should be the <a href> that they clicked on
var assignmentId = $(this).parents('tr').data('assignment-id');
var userId = $(this).parents('tr').data('user-id');
var grade = $(this).data('grade').toString().replace('--', '');
var url = $('.update_submission_grade_url').attr('href');
var method = $('.update_submission_grade_url').attr('title');
event.preventDefault();
$('.assignment_' + assignmentId + '_user_' + userId + '_current_grade').addClass('loading');
var formData = {
'submission[assignment_id]': assignmentId,
'submission[user_id]': userId
};
if(grade == "EX") {
formData['submission[excused]'] = 1;
} else {
formData['submission[grade]'] = grade;
}
$.ajaxJSON(url, method, formData, function(submissions) {
var currentGradeText;
$.each(submissions, function () {
var submission = this.submission;
var el = $('.assignment_' + submission.assignment_id + '_user_' + submission.user_id + '_current_grade');
el.removeClass('loading');
el.attr('title', I18n.t('graded_by_me', "%{graded_time} by me", { 'graded_time': $.datetimeString(submission.graded_at) }));
if (submission.excused) {
currentGradeText = 'EX';
} else {
currentGradeText = GradeFormatHelper.formatGrade(submission.grade) || '--';
}
el.text(currentGradeText);
});
announceUpdatedCurrentGrade(currentGradeText);
});
}
};
export default GradebookHistory;

View File

@ -1,108 +0,0 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import GradebookHistory from 'gradebook-history';
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const fixtures = document.getElementById('fixtures');
function revertGradeHTML ({grade, excused}) {
let displayGrade = grade;
if (excused) {
displayGrade = 'EX';
} else if (grade === null) {
displayGrade = '-';
}
return (
<div>
<a style={{ display: 'none' }} title="POST" href="someLink" className="update_submission_grade_url" />
<table>
<tbody>
<tr data-assignment-id="140" data-user-id="4">
<td><a href="#" className="ui-corner-all revert-grade-link" data-grade={displayGrade} /></td>
<td>
<span title="Thursday by cletus@example.com" className="current_grade assignment_140_user_4_current_grade">
{displayGrade}
</span>
</td>
</tr>
</tbody>
</table>
</div>
);
}
function submissionsResponse ({grade, excused}) {
const response = JSON.stringify([{
submission: {
id: '5',
grade,
excused,
assignment_id: '140',
user_id: '4',
graded_at: '2016-03-16T17:40:40Z'
}
}]);
return [200, { 'Content-Type': 'application/json' }, response];
}
QUnit.module('GradebookHistory', {
setupTest ({grade, excused}) {
ReactDOM.render(revertGradeHTML({ grade, excused }), fixtures);
this.stub($, 'screenReaderFlashMessage');
this.server = sinon.fakeServer.create();
this.server.respondWith('POST', 'someLink', submissionsResponse({ grade, excused }));
GradebookHistory.init();
},
teardown () {
this.server.restore();
fixtures.innerHTML = '';
}
});
test('flashes a screenreader message when "Revert to this grade" is clicked', function () {
this.setupTest({ grade: '5', excused: false });
document.querySelector('.revert-grade-link').click();
this.server.respond();
ok($.screenReaderFlashMessage.calledOnce);
});
test('notifies the user of the new grade', function () {
const grade = '5';
this.setupTest({ grade, excused: false });
document.getElementsByClassName('revert-grade-link')[0].click();
this.server.respond();
ok($.screenReaderFlashMessage.calledWithExactly(`Updated current grade to ${grade}`));
});
test('notifies the user of new empty grades', function () {
this.setupTest({ grade: null, excused: false });
document.getElementsByClassName('revert-grade-link')[0].click();
this.server.respond();
ok($.screenReaderFlashMessage.calledWithExactly('Updated current grade to be empty'));
});
test('notifies the user of new excused grades', function () {
this.setupTest({ grade: null, excused: true });
document.getElementsByClassName('revert-grade-link')[0].click();
this.server.respond();
ok($.screenReaderFlashMessage.calledWithExactly('Updated current grade to EX'));
});

View File

@ -1,66 +0,0 @@
#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../../helpers/gradebook_common'
describe "gradebook - Grading History" do
include GradebookCommon
include_context 'in-process server selenium tests'
include_context 'reusable_course'
context 'Grading History' do
before(:each) do
enroll_teacher_and_students
student_submission
user_session(teacher)
end
it 'toggles and displays grading history', priority: "2", test_id: 602872 do
assignment_1.grade_student(student, grade: 8, grader: teacher)
assignment_1.grade_student(student, grade: 10, grader: teacher)
get "/courses/#{test_course.id}/gradebook/history"
# expand grade history toggle
f('.assignment_header a').click
wait_for_animations
current_grade_column = fj(".current_grade.assignment_#{assignment_1.id}_user_#{student.id}_current_grade")
expect(current_grade_column).to include_text('10')
end
it 'displays and reverts excused grades', priority: "1", test_id: 606308 do
assignment_1.grade_student(student, excuse: true, grader: teacher)
assignment_1.grade_student(student, grade: 15, grader: teacher)
get "/courses/#{test_course.id}/gradebook/history"
f('.assignment_header').click
wait_for_ajaximations
expect(f('.assignment_header .changes').text).to eq '1 change'
changed_values = ff('.assignment_details td').map(& :text)
expect(changed_values).to eq ['EX', '15', '15']
assignment_1.grade_student(student, grade: 10, grader: teacher)
refresh_page
f('.assignment_header').click
wait_for_ajaximations
changed_values = ff('.assignment_details td').map(& :text)
expect(changed_values).to eq ['15', '10', '10']
end
end
end

View File

@ -1,66 +0,0 @@
#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../../helpers/gradezilla_common'
describe "Gradezilla - Grading History" do
include GradezillaCommon
include_context 'in-process server selenium tests'
include_context 'reusable_course'
context 'Grading History' do
before(:each) do
enroll_teacher_and_students
student_submission
user_session(teacher)
end
it 'toggles and displays grading history', priority: "2", test_id: 602872 do
assignment_1.grade_student(student, grade: 8, grader: teacher)
assignment_1.grade_student(student, grade: 10, grader: teacher)
get "/courses/#{test_course.id}/gradebook/history"
# expand grade history toggle
f('.assignment_header a').click
wait_for_animations
current_grade_column = fj(".current_grade.assignment_#{assignment_1.id}_user_#{student.id}_current_grade")
expect(current_grade_column).to include_text('10')
end
it 'displays and reverts excused grades', priority: "1", test_id: 606308 do
assignment_1.grade_student(student, excuse: true, grader: teacher)
assignment_1.grade_student(student, grade: 15, grader: teacher)
get "/courses/#{test_course.id}/gradebook/history"
f('.assignment_header').click
wait_for_ajaximations
expect(f('.assignment_header .changes').text).to eq '1 change'
changed_values = ff('.assignment_details td').map(& :text)
expect(changed_values).to eq ['EX', '15', '15']
assignment_1.grade_student(student, grade: 10, grader: teacher)
refresh_page
f('.assignment_header').click
wait_for_ajaximations
changed_values = ff('.assignment_details td').map(& :text)
expect(changed_values).to eq ['15', '10', '10']
end
end
end