Skip to content

Commit

Permalink
Dynamic bootstrap tabs with detatch()
Browse files Browse the repository at this point in the history
  • Loading branch information
NatTuck committed May 12, 2017
1 parent 5525b66 commit 04f258f
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 249 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -56,6 +56,7 @@ group :development do
end

group :development, :test do
gem 'puma'
gem 'pry'
gem 'pry-rails'
end
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Expand Up @@ -183,6 +183,7 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (2.0.5)
puma (3.8.2)
rack (2.0.1)
rack-test (0.6.3)
rack (>= 1.0)
Expand Down Expand Up @@ -306,6 +307,7 @@ DEPENDENCIES
pretender
pry
pry-rails
puma
rack
rails (~> 5.1)
rails-controller-testing
Expand Down
5 changes: 1 addition & 4 deletions app/assets/javascripts/assignments.js
Expand Up @@ -9,13 +9,10 @@

function on_add_grader(evt, el) {
el.find(".spinner").each(function(_ii, div) {
// FIXME: Doesn't actually find spinners.
activateSpinner(div);
});

form_tabs_init(el);

console.log(el);
form_tabs_init_all(el);
}

function form_init() {
Expand Down
106 changes: 60 additions & 46 deletions app/assets/javascripts/form-tabs.js
@@ -1,71 +1,85 @@
/*
.form-tabs <- data-init-tab = default tab
.nav-tabs
.li a ... <- click this
.tab-pane ... <- to find these
.form-group <- and swap these, so only one exists at at a time
Both "a" and ".tab-pane" should be marked with matching data-tab attrs.
*/


window.form_tabs_init = function (tabs_div) {
var top = $(tabs_div);
var val0 = top.data('type');
var val0 = top.data('init-tab');
var tabs = {};

function show_tab(ty) {
function show_tab(tab) {
console.log("show", tab);

top.find('.nav-tabs a').each(function (_ii, lnk) {
if (ty == $(lnk).data('type')) {
$(lnk).addClass('active');
var this_tab = $(lnk).data('tab');
var item = $($(lnk).closest('li'));

This comment has been minimized.

Copy link
@blerner

blerner May 12, 2017

The outer $(...) is redundant: jQuery always returns you a query object


if (tab == this_tab) {
item.addClass('active');
}
else {
$(lnk).removeClass('active');
item.removeClass('active');
}
});

FIXME: Finish this plan.
top.find('.tab-pane')

var tp = $($(tlnk).data('target'))[0];
console.log("hide", tp);

var uuid = $(tp).closest(".form-tabs-pane").data("uuid");
tabs[uuid] = tabs[uuid] || {};

var pid = tp.id;
var fmg = $(tp).find(".form-group")[0];
if (fmg) {
console.log("Storing to", uuid, pid);
tabs[uuid][pid] = fmg;
$(fmg).detach();
}
}

function show_tab(tlnk) {
var tp = $($(tlnk).data('target'))[0];
console.log("show", tp);

var uuid = $(tp).closest(".form-tabs-pane").data("uuid");
tabs[uuid] = tabs[uuid] || {};
top.find('.tab-pane').each(function (_ii, div) {
var this_tab = $(div).data('tab');

This comment has been minimized.

Copy link
@blerner

blerner May 12, 2017

Probably worth var $div = $(div), since you use it frequently below. Similarly for $lnk


var pid = tp.id;
var fmg = tabs[uuid][pid];
$(tp).append(fmg);
if (tab == this_tab) {
$(div).addClass('active');
$(div).show();
if (tabs[this_tab]) {
console.log("restored", tabs[tab]);
$(div).append(tabs[tab]);
}
}
else {
var fg = $(div).find('.form-group');
if (fg[0]) {
console.log("saved", fg[0]);
tabs[this_tab] = fg[0];
$(fg[0]).detach();
}
$(div).removeClass('active');
$(div).hide();
}
});
}

top.find(".nav-tabs a").each(function(_ii, lnk) {
if (val0 == $(lnk).data('type')) {
$(lnk).click();
}
});
// Match height before we hide anything.
var panes = top.find('.tab-pane');
panes.matchHeight({byRow: false, property: 'height'});

top.find(".nav-tabs a").each(function(_ii, lnk) {
if (val0 != $(lnk).data('type')) {
hide_tab(lnk);
var tab = $(lnk).data('tab');
if (tab == val0) {
show_tab(tab);
}

$(lnk).on("show.bs.tab", function(evt) {
show_tab(evt.target);
});
$(lnk).on("hide.bs.tab", function(evt) {
hide_tab(evt.target);
$(lnk).on("click", function(evt) {
evt.preventDefault();
show_tab(tab);
});
});

};

$(function() {
$('.form-tabs-pane').each(function (_ii, el) {
window.form_tabs_init_all = function (thing) {
$(thing).find('.form-tabs').each(function (_ii, el) {
form_tabs_init(el);
});
};

$(function() {
form_tabs_init_all('body');
});

164 changes: 0 additions & 164 deletions app/controllers/assignments_controller.rb
Expand Up @@ -253,170 +253,6 @@ def show_Exam
@grader = @assignment.graders.first
end

# setup graders
def set_questions_graders
# FIXME: This is way too complicated.

upload = params[:assignment][:assignment_file]
if upload.nil?
if @assignment.assignment_upload.nil?
@assignment.errors.add(:base, "Assignment questions file is missing")
return false
else
return true
end
else
begin
questions = YAML.load(upload.tempfile)
upload.rewind
rescue Psych::SyntaxError => e
@assignment.errors.add(:base, "Could not parse the supplied file")
return false
end
end
if !questions.is_a? Array
@assignment.errors.add(:base, "Supplied file does not contain a list of sections")
return false
else
@question_count = 0
@total_weight = 0
question_kinds = Assignment.question_kinds.keys
@no_problems = true
def make_err(msg)
@assignment.errors.add(:base, msg)
@no_problems = false
end
def is_float(val)
Float(val) rescue false
end
def is_int(val)
Integer(val) rescue false
end
questions.each_with_index do |section, index|
if !(section.is_a? Object) || !(section.keys.is_a? Array) || section.keys.count > 1
make_err "Section #{index} is malformed"
next
else
section.each do |secName, sec_questions|
sec_questions.each do |question|
question.each do |type, q|
@question_count += 1
begin
if !(type.is_a? String)
make_err "Question #{@question_count} (in section #{secName}) has unknown type #{type}"
next
elsif !question_kinds.member?(type.underscore)
make_err "Question #{@question_count} (in section #{secName}) has unknown type #{type}"
next
else
if q["weight"].nil? or !(Float(q["weight"]) rescue false)
make_err "Question #{@question_count} has missing or invalid weight"
end
@total_weight += Float(q["weight"])
ans = q["correctAnswer"]
if ans.nil?
make_err "Question #{@question_count} is missing a correctAnswer"
end
if q["rubric"].nil?
make_err "Question #{@question_count} is missing a rubric"
elsif !(q["rubric"].is_a? Array)
make_err "Question #{@question_count} has an invalid rubric"
else
q["rubric"].each_with_index do |guide, i|
if !(guide.is_a? Object) or guide.keys.count != 1
make_err "Question #{@question_count}, rubric entry #{i} is ill-formed"
else
guide.each do |weight, hint|
if !(Float(weight) rescue false)
make_err "Question #{@question_count}, rubric entry #{i} has non-numeric weight"
elsif Float(weight) < 0 or Float(weight) > 1
make_err "Question #{@question_count}, rubric entry #{i} has out-of-bounds weight"
end
end
end
end
end
if q["prompt"].nil?
make_err "Question #{@question_count} is missing a prompt"
end
case type
when "YesNo", "TrueFalse"
if ![true, false].member?(q["correctAnswer"])
make_err "Boolean question #{@question_count} has a non-boolean correctAnswer"
end
when "Numeric"
min = q["min"]
max = q["max"]
if max.nil? or !is_float(min)
make_err "Numeric question #{@question_count} has a non-numeric max"
else
max = max.to_f
end
if min.nil? or !is_float(min)
make_err "Numeric question #{@question_count} has a non-numeric min"
else
min = min.to_f
end
if ans.nil? or !is_float(ans)
make_err "Numeric question #{@question_count} has a non-numeric ans"
else
ans = ans.to_f
end
if is_float(min) and is_float(max) and is_float(ans) and !(min <= ans and ans <= max)
make_err "Numeric question #{@question_count} has a correctAnswer outside the specified range"
end
when "MultipleChoice"
if q["options"].nil? or !q["options"].is_a? Array
make_err "MultipleChoice question #{@question_count} is missing an array of choices"
end
if !is_int(ans)
make_err "MultipleChoice question #{@question_count} has a non-numeric correctAnswer"
else
ans = ans.to_i
end
if is_int(ans) and (ans < 0 or ans >= q["options"].count)
make_err "MultipleChoice question #{@question_count} has a correctAnswer not in the available choices"
end
end
if q["parts"]
if !q["parts"].is_a? Array
make_err "Question #{@question_count} has a non-list of parts"
else
q["parts"].each_with_index do |part, part_i|
if !part.is_a? Object
make_err "Question #{@question_count} has a non-object part ##{part_i + 1}"
elsif part.keys.count > 1
make_err "Question #{@question_count} part ##{part_i + 1} has too many keys"
elsif !["codeTag", "codeTags", "requiredText", "text"].member?(part.keys[0])
make_err "Question #{@question_count} part ##{part_i + 1} has an invalid type #{part.keys[0]}"
end
end
end
end
end
rescue Exception => e
make_err "Question #{@question_count} in section #{secName} could not be parsed: #{e}"
end
end
end
end
end
end
end
if @no_problems
c = Grader.new(type: "QuestionsGrader", avail_score: @total_weight)
if c.invalid? or !c.save
no_problems = false
@assignment.errors.add(:graders, "Could not create grader #{c.to_s}")
else
AssignmentGrader
.find_or_initialize_by(assignment_id: @assignment.id)
.update(order: 1, grader_id: c.id)
end
end
return @no_problems
end

def set_files_graders
params_graders = graders_params
if params_graders.nil?
Expand Down
2 changes: 1 addition & 1 deletion app/views/courses/_form.html.erb
Expand Up @@ -46,7 +46,7 @@
</div>
<div class="form-group">
<p><strong>Late Penalty</strong></p>
<%= render 'lateness/picker', within_course: !@course.new_record?, f: f %>
<%= render 'lateness/picker', within_course: false, f: f %>
</div>
<div>
<%= f.label :footer, "Course-Wide Footer" %> (HTML Allowed)
Expand Down

2 comments on commit 04f258f

@blerner
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Irony: the remove buttons work for existing graders, but do not work for the new ones generated by the add-grader button.

@blerner
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, actually, the remove button for existing graders does not delete the <li> items, which means the numbers stay screwed up. Additionally, the remove buttons allow you to remove the final grader and update the assignment, but no assignment should be permitted to have zero graders.

Please sign in to comment.