Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E1929. Visualizations for Instructors #1440

Closed
wants to merge 90 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
4be091b
Fix spelling errors to generate pull request to expertiza/beta.
hughgs Apr 16, 2019
f591704
First design commit. Create partials for each of the graphs. The rubr…
gshugh Apr 19, 2019
3354500
Merge remote-tracking branch 'origin/OSS-HW4' into add_to_view
gshugh Apr 19, 2019
af442b4
First cut at tests for the rubric stat display.
hughgs Apr 19, 2019
ca3de27
Changed some wording to better reflect the behavior as default rather…
hughgs Apr 19, 2019
70f25ab
First attempt at setting up the plumbing between grades_controller an…
bobbydemarco Apr 20, 2019
534bfbf
Finish the RSpec outline. Add helper method.
hughgs Apr 20, 2019
04f5a1d
Mock data in grades_controller successfully read in in the view. Drop…
bobbydemarco Apr 20, 2019
39e6173
Merge branches 'OSS-HW4' and 'rdemarc' of https://github.com/gshugh/e…
gshugh Apr 20, 2019
4008dae
Rename table data id to better describe it, in preparation for refact…
gshugh Apr 20, 2019
d5db184
Use controller instance variables in the JavaScript and the controlle…
gshugh Apr 21, 2019
a9dfee8
Bar graph has mysteriously disappeared. Hoping someone else can help.
gshugh Apr 21, 2019
0e1700b
Add questionnaire rounds method to Assignment
jwarren3 Apr 21, 2019
8ae64dc
Fix Ruby syntax
jwarren3 Apr 21, 2019
53fbedc
Merge pull request #27 from gshugh/jwarren3
jwarren3 Apr 21, 2019
3235a81
Remove static strings from menus. Ensure that controller instance var…
gshugh Apr 21, 2019
f897462
Merge branches 'gshugh' and 'OSS-HW4' of https://github.com/gshugh/ex…
gshugh Apr 21, 2019
e31eb4e
Mock stats implementation
jwarren3 Apr 21, 2019
2fc3dcb
Merge remote-tracking branch 'origin/OSS-HW4' into jwarren3
jwarren3 Apr 21, 2019
4419963
More mock
jwarren3 Apr 21, 2019
c13a7eb
Merge pull request #28 from gshugh/jwarren3
jwarren3 Apr 21, 2019
06bed3c
Remove the hard-coded string arrays to create the menus. Use the scor…
gshugh Apr 22, 2019
fb51cc5
Fix some Code Climate issues. Mostly space and newline complaints.
gshugh Apr 22, 2019
2a5e2cd
Fixing the last Code Climate issues.
gshugh Apr 22, 2019
8a12b56
Redo rubric score structure per conversation with John Warren.
gshugh Apr 22, 2019
fb763ea
Fix some Code Climate spacing issues.
gshugh Apr 22, 2019
fcf9c60
Add Enumerable to new classes so that we can iterate through their li…
gshugh Apr 22, 2019
993debe
Fix Code Climate spacing issues.
gshugh Apr 22, 2019
40773c1
Fix enumerable issue. Add name to AssignmentStats structure.
gshugh Apr 23, 2019
b28a148
Fix Code Climate spacing issues.
gshugh Apr 23, 2019
2c42d19
Fix Code Climate spacing issues. Again.
gshugh Apr 23, 2019
9f3dae7
Move structure specific data back to models. Load means and medians i…
gshugh Apr 23, 2019
496af65
Add retrieval of metrics from criterion_stats. Incorporate the array …
gshugh Apr 23, 2019
47012a3
Add stats helper
jwarren3 Apr 24, 2019
25c6d98
Wire in AssignmentStatsHelper
jwarren3 Apr 24, 2019
fec5c19
Working AssignmentStats
jwarren3 Apr 24, 2019
f9b94a9
Enable live data in rubric chart
jwarren3 Apr 24, 2019
f2946cd
Refactor the code so that methods in a helper file are used so that t…
gshugh Apr 24, 2019
87d1a13
Fix some of the Code Climate issues associated with the initial push …
gshugh Apr 24, 2019
e20b1bb
Hope I got the last Code Climate issues.
gshugh Apr 24, 2019
fb6b357
added RSpec/Capybara to verify grade chart
cladkins-ncsu Apr 24, 2019
5f3408c
Merge branch 'OSS-HW4' of https://github.com/gshugh/expertiza into OS…
cladkins-ncsu Apr 24, 2019
45267ab
Hope I got the last Code Climate issues.
gshugh Apr 24, 2019
55ad769
Hope I got the last Code Climate issue.
gshugh Apr 25, 2019
5cfe0e5
Remove some left-over comments and other issues that John found.
gshugh Apr 25, 2019
18e9cf0
Merge branch 'OSS-HW4' of https://github.com/gshugh/expertiza into stats
gshugh Apr 25, 2019
0086d18
Forgot to remove questionnaire_rounds.
gshugh Apr 25, 2019
6598c92
updated grade_interface_spec AssignmentQuestionaire seed
cladkins-ncsu Apr 25, 2019
52b0b8e
Add comparison query logic
jwarren3 Apr 25, 2019
bac4ef8
updated GEM file for test environment
cladkins-ncsu Apr 25, 2019
900d0b6
updated GEM file for test environment
cladkins-ncsu Apr 25, 2019
1658a5d
Force menu items to revert to default when the page is reloaded from …
gshugh Apr 26, 2019
d350ebe
Merge branch 'gshugh' of https://github.com/gshugh/expertiza into UI_…
gshugh Apr 26, 2019
ea61e9b
Remove sessionStorage reference.
gshugh Apr 26, 2019
c3531a3
Fix Code Climate issues.
gshugh Apr 26, 2019
b423c64
Add comparison rubrics. Fixed Code Climate issues.
gshugh Apr 26, 2019
9cc0b56
Created rubric_stats.js and made first attempt to move code out of _t…
bobbydemarco Apr 26, 2019
403a3d9
Fix failed test.
gshugh Apr 26, 2019
198a789
Don't allow the user to select compare if there are no rubrics to com…
gshugh Apr 26, 2019
3430fac
Fix padding around menu selections at bottom of UI.
gshugh Apr 26, 2019
005b2d2
Update view code to use HTML5.
hughgs Apr 26, 2019
2251a55
Merge remote-tracking branch 'origin/gshugh' into gshugh
hughgs Apr 26, 2019
c1b00d9
working rspec testing
cladkins-ncsu Apr 26, 2019
788244d
working rspec testing
cladkins-ncsu Apr 26, 2019
c79c4fc
update seeded scores
cladkins-ncsu Apr 26, 2019
1c3dc8e
Merge branch 'OSS-HW4' of https://github.com/gshugh/expertiza into OS…
cladkins-ncsu Apr 26, 2019
4181072
Another attempt at getting the javascript page loaded in the view.
bobbydemarco Apr 26, 2019
ee0528a
updated test database to add second assignment
cladkins-ncsu Apr 26, 2019
e42236d
removed byebug from GEM file
cladkins-ncsu Apr 26, 2019
11f9607
Put back the top line where it seemed to load in the google charts ja…
bobbydemarco Apr 26, 2019
869b5e2
Modified path to try to load the rubric_stats.js page.
bobbydemarco Apr 27, 2019
f308e60
Put dynamic bits back into rubric partial
jwarren3 Apr 27, 2019
5bad320
Merge branches 'gshugh' and 'rdemarc' of https://github.com/gshugh/ex…
hughgs Apr 27, 2019
2fe7e6f
Move loading of google.charts back to the view.
hughgs Apr 27, 2019
7e43f59
Move more variables back to view to remove ReferenceErrors in JavaScr…
hughgs Apr 27, 2019
0673448
Remove commented code.
hughgs Apr 27, 2019
ee59f57
Merge branch 'rdemarc' into OSS-HW4
bobbydemarco Apr 27, 2019
83fc37d
removed duplicate attribute from factories.rb
cladkins-ncsu Apr 27, 2019
a8465ca
Remove webdriver gem and remove superfluous test.
gshugh Apr 27, 2019
07e562d
Remove headless webdriver gem lines. Fix use of database_cleaner gem …
gshugh Apr 28, 2019
b090071
Fix more Code Climate issues.
gshugh Apr 28, 2019
ddd2ad3
Fix more Code Climate issues.
gshugh Apr 28, 2019
2325f71
Fix more Code Climate issues.
gshugh Apr 28, 2019
f843444
Remove headless webdriver gem lines.
gshugh Apr 28, 2019
c3352a9
Remove gems introduced during testing.
gshugh Apr 28, 2019
5790414
Remove gems introduced during testing.
gshugh Apr 28, 2019
a8e3a64
Fix minor UI bugs
jwarren3 Apr 30, 2019
b2df51b
Add pending tests to grade_interface_spec. Remove comments from grade…
gshugh May 7, 2019
03323e0
Merge branch 'OSS-HW4' of https://github.com/gshugh/expertiza into temp
gshugh May 7, 2019
d201982
Fix assignment #3 id.
gshugh May 7, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
135 changes: 135 additions & 0 deletions app/assets/javascripts/rubric_stats.js
@@ -0,0 +1,135 @@

function buildSelectArray(prefix, length) {
var selected = [];
for (i = 0; i < length; i++) {
var eID = prefix+"_"+i+"_selector";
var selector = document.getElementById(eID);
selected.push(selector.checked);
}
return selected;
}

var selected_round = 0;
function toggleRound(control) {
selected_round = control.selectedIndex;
for (var round = 0; round < round_names.length; round++) {
var roundDiv = document.getElementById("round_"+round+"_criteria");
var display = null;
if (round != selected_round) display = "none";
roundDiv.style.display = display;
}
drawChart();
}

var selected_assignment_round = 0;
function toggleAssignmentRound(control) {
selected_assignment_round = control.selectedIndex;
renderCompareUI();
}

function renderCompareUI() {
for (var assignment = 0; assignment < assignment_names_to_compare.length; assignment++) {
for (var round = 0; round < round_names.length; round++) {
eID = "assignment_"+assignment+"_round_"+round+"_criteria";
var roundDiv = document.getElementById(eID);
var display = null;
if (assignment != selected_assignment || round != selected_assignment_round) display = "none";
roundDiv.style.display = display;
}
}
drawChart();
}

function toggleMetric(control) {
var data = [avg_data, med_data];
source_data = data[control.selectedIndex];
drawChart();
}

function toggleAssignment(control) {
selected_assignment = control.selectedIndex;
renderCompareUI();
}

// Callback that creates and populates a data table,
// instantiates the column chart, passes in the data and
// draws it.
function drawSingleChart() {
var selected_criteria = buildSelectArray("round_"+selected_round+"_criterion", criteria_names[selected_round].length);

// Create the data table.
var data = new google.visualization.DataTable();
data.addColumn('string', 'Criteria');
data.addColumn('number', round_names[selected_round]);
var rows = [];
for (criterion = 0; criterion < selected_criteria.length; criterion++) {
if (!selected_criteria[criterion]) {
continue;
}
var row = [criteria_names[selected_round][criterion]];
row.push(source_data[selected_round][criterion]);
rows.push(row);
}
data.addRows(rows);
// Set chart options
var options = {
'title':'Class Performance By Criterion',
'width':700,
'height':250
};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
chart.draw(data, options);
}

// Callback that creates and populates a data table,
// instantiates the column chart, passes in the data and
// draws it.
function drawCompareChart() {
source_data = avg_data;
var prefix = "assignment_"+selected_assignment+"_round_"+selected_assignment_round+"_criterion";
var selected_criteria = buildSelectArray(prefix, criteria_names[selected_assignment_round].length);

// Create the data table.
var data = new google.visualization.DataTable();
data.addColumn('string', 'Criteria');
data.addColumn('number', assignment_name);
data.addColumn('number', assignment_names_to_compare[selected_assignment]);
var rows = [];
for (criterion = 0; criterion < selected_criteria.length; criterion++) {
if (!selected_criteria[criterion]) {
continue;
}
var row = [criteria_names[selected_assignment_round][criterion]];
row.push(avg_data[selected_assignment_round][criterion]);
row.push(avg_data_to_compare[selected_assignment][selected_assignment_round][criterion]);
rows.push(row);
}
data.addRows(rows);
// Set chart options
var options = {
'title':'Class Performance By Criterion For ' + round_names[selected_assignment_round],
'width':700,
'height':250
};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
chart.draw(data, options);
}

var drawChart = drawSingleChart;

var tabCount = 2;
function switchToTab(newTab) {
var chartFuncs = [drawSingleChart, drawCompareChart];
for (tab = 0; tab < tabCount; tab++) {
var display = null;
if (tab != newTab) display = "none";
var selector = document.getElementById("tab_"+tab+"_selector").style;
selector.display = display;
var tabDiv = document.getElementById("tab_"+tab).style;
tabDiv.display = display;
}
drawChart = chartFuncs[newTab];
drawChart();
}
19 changes: 18 additions & 1 deletion app/controllers/grades_controller.rb
Expand Up @@ -6,6 +6,7 @@ class GradesController < ApplicationController
include StudentTaskHelper
include AssignmentHelper
include GradesHelper
include AssignmentStatsHelper

def action_allowed?
case params[:action]
Expand Down Expand Up @@ -50,12 +51,28 @@ def view
end

@scores = @assignment.scores(@questions)
averages = calculate_average_vector(@assignment.scores(@questions))
averages = calculate_average_vector(@scores)
@average_chart = bar_chart(averages, 300, 100, 5)
@avg_of_avg = mean(averages)
calculate_all_penalties(@assignment.id)

@show_reputation = false

# Define instance variables for rubric visualization
stats = AssignmentStats.new(@assignment.id)

@avg_data = mean_data(stats)
@med_data = median_data(stats)
@metric_names = %w[Mean Median]

@assignment_name = @assignment.name
@round_names = round_names(stats)
@criteria_names = criteria_names(stats)

comparable_assignment_stats = @assignment.comparable_assignment_ids.map {|id| AssignmentStats.new(id) }

@assignment_avg_data = comparison_mean_data(comparable_assignment_stats)
@assignment_names = comparison_assignment_names(comparable_assignment_stats)
end

def view_my_scores
Expand Down
33 changes: 33 additions & 0 deletions app/helpers/assignment_stats_helper.rb
@@ -0,0 +1,33 @@
module AssignmentStatsHelper
def mean_data(assignment_stats)
assignment_stats.rounds.map(&:means)
end

def median_data(assignment_stats)
assignment_stats.rounds.map(&:medians)
end

def criteria_names(assignment_stats)
rounds = []
assignment_stats.rounds.each do |r|
rounds << (1..r.number_of_criteria).map {|c| "Criterion #{c}" }
end
rounds
end

def round_names(assignment_stats)
(1..assignment_stats.number_of_rounds).map {|r| "Round #{r}" }
end

def comparison_mean_data(assignment_stats_array)
data = []
assignment_stats_array.each do |as|
data << as.rounds.map(&:means)
end
data
end

def comparison_assignment_names(assignment_stats_array)
assignment_stats_array.map(&:name)
end
end
20 changes: 20 additions & 0 deletions app/models/assignment.rb
Expand Up @@ -419,6 +419,26 @@ def review_questionnaire_id(round = nil)
review_questionnaire_id
end

# Provides an array of all comparable assignments. Those are assignments that are part of the same course
# and have assignment questionnaires with matching rounds and questionnaire IDs.
def comparable_assignment_ids
comparable_assignments = Assignment.where(course_id: self.course_id).reject {|a| a.id == self.id }
comparable_assignment_ids = comparable_assignments.map(&:id)
# Maps round identifiers to questionnaire IDs in this assignment
round_questionnaire_hash = {}
aqs_with_round = AssignmentQuestionnaire.where(assignment_id: self.id).reject {|q| q.used_in_round.nil? }
aqs_with_round.each {|aq| round_questionnaire_hash[aq.used_in_round] = aq.questionnaire_id }

# Find all assignments with the same questionnaires used in the same rounds and get the intersection
# with assignments in the same course retrieved above.
round_questionnaire_hash.each do |round, questionnaire|
matching_aqs = AssignmentQuestionnaire.where(used_in_round: round, questionnaire_id: questionnaire)
assignment_ids = matching_aqs.map(&:assignment_id)
comparable_assignment_ids &= assignment_ids
end
comparable_assignment_ids
end

def self.export_details(csv, parent_id, detail_options)
return csv unless detail_options.value?('true')
@assignment = Assignment.find(parent_id)
Expand Down
3 changes: 3 additions & 0 deletions app/models/assignment_questionnaire.rb
Expand Up @@ -6,4 +6,7 @@ class AssignmentQuestionnaire < ActiveRecord::Base
scope :retrieve_questionnaire_for_assignment, lambda {|assignment_id|
joins(:questionnaire).where('assignment_questionnaires.assignment_id = ?', assignment_id)
}
def question_ids_in_order
self.questionnaire.question_ids_in_order
end
end
56 changes: 56 additions & 0 deletions app/models/assignment_stats.rb
@@ -0,0 +1,56 @@
class AssignmentStats
attr_accessor :rounds, :name

def initialize(assignment_id)
@name = Assignment.find(assignment_id).name
# These represent rounds
aqs_with_round = AssignmentQuestionnaire.where(assignment_id: assignment_id).reject {|q| q.used_in_round.nil? }
aqs_with_round.sort_by!(&:used_in_round)
# This hash maps question IDs to their zero-indexed positions within their questionnaire
question_id_index_hash = {}
aqs_with_round.each do |q|
q.question_ids_in_order.each_with_index {|value, index| question_id_index_hash[value] = index }
end
# question_id_index_hash
# This hash maps rounds to criteria to scores, max_scores and min_scores
scores_hash = {}
review_response_maps = ReviewResponseMap.where(reviewed_object_id: assignment_id)
review_response_maps.each do |review_response_map|
submitted_responses = review_response_map.response.reject {|r| !r.is_submitted }
submitted_responses.each do |response|
criteria_hash = scores_hash[response.round]
if criteria_hash.nil?
criteria_hash = {}
scores_hash[response.round] = criteria_hash
end
answers = response.scores.reject {|s| s.answer.nil? }
answers.each do |answer|
criterion_hash = criteria_hash[answer.question_id]
if criterion_hash.nil?
criterion_hash = {}
criteria_hash[answer.question_id] = criterion_hash
end
c_scores = criterion_hash[:scores]
if c_scores.nil?
c_scores = []
criterion_hash[:scores] = c_scores
end
c_scores << answer.answer
next unless criterion_hash[:max_score].nil?
question = Question.find(answer.question_id)
criterion_hash[:question_id] = answer.question_id
criterion_hash[:max_score] = question.max_score
criterion_hash[:min_score] = question.min_score
end
end
end
@rounds = []
scores_hash.each_value do |criteria_hash|
@rounds << ReviewRoundStats.new(criteria_hash, question_id_index_hash)
end
end

def number_of_rounds
@rounds.length
end
end
14 changes: 14 additions & 0 deletions app/models/criterion_stats.rb
@@ -0,0 +1,14 @@
class CriterionStats
attr_accessor :mean, :median

def initialize(criterion_hash)
criterion_hash[:scores].sort!
s = criterion_hash[:scores]
@median = (s[s.length / 2].to_f + s[(s.length - 1) / 2].to_f) / 2.0
mean = s.inject(&:+).to_f / s.length # {|sum, el| sum + el }.to_f / s.length
max = criterion_hash[:max_score]
min = criterion_hash[:min_score]
normalized_mean = (mean - min.to_f) / (max.to_f - min.to_f)
@mean = normalized_mean * 100
end
end
8 changes: 8 additions & 0 deletions app/models/question.rb
Expand Up @@ -52,6 +52,14 @@ def get_formatted_question_type
end
end

def max_score
self.questionnaire.max_question_score
end

def min_score
self.questionnaire.min_question_score
end

# Placeholder methods, override in derived classes if required.
# this method decide what to display if an instructor (etc.) is creating or editing a questionnaire
def edit
Expand Down
5 changes: 5 additions & 0 deletions app/models/questionnaire.rb
Expand Up @@ -86,4 +86,9 @@ def validate_questionnaire
results = Questionnaire.where("id <> ? and name = ? and instructor_id = ?", id, name, instructor_id)
errors.add(:name, "Questionnaire names must be unique.") if results.present?
end

def question_ids_in_order
sorted_questions = self.questions.to_a.sort_by!(&:seq)
sorted_questions.map(&:id)
end
end
22 changes: 22 additions & 0 deletions app/models/review_round_stats.rb
@@ -0,0 +1,22 @@
class ReviewRoundStats
attr_accessor :criteria

def initialize(criteria_hash, question_id_index_hash)
@criteria = []
criteria_hash.each do |question, criterion_hash|
@criteria[question_id_index_hash[question]] = CriterionStats.new(criterion_hash) unless question.nil? || question_id_index_hash[question].nil?
end
end

def means
@criteria.map(&:mean)
end

def medians
@criteria.map(&:median)
end

def number_of_criteria
@criteria.length
end
end
2 changes: 1 addition & 1 deletion app/models/score_view.rb
@@ -1,5 +1,5 @@
class ScoreView < ActiveRecord::Base
def readonly?
true
false
end
end