edit/delete mult enroll on course settings page; fixes #6996

We want to be able to change or delete enrollments on the course settings page,
even if there are multiple enrollments for a single student.  This commit adds
this ability, and adds tests to ensure that multiple enrollments are coalesed
under a single user.

test-plan:
- create a course with three sections
- enroll a student in two of these sections (from the console)
- navigate to the course settings page, users tab
- make sure the student only shows up once, with both enrollments listed
  underneath
- try changing one of these enrollments to the third section.
- try deleting one, and then both of these enrollments

Change-Id: I4bc35ac2f79fd6297b50e5217a567290eff59a92
Reviewed-on: https://gerrit.instructure.com/8364
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Simon Williams 2012-01-27 15:17:42 -07:00
parent a30a1b8d9f
commit e68f1beecc
8 changed files with 152 additions and 61 deletions

View File

@ -859,15 +859,22 @@ class CoursesController < ApplicationController
@enrollment = @context.enrollments.find(params[:id])
can_move = [StudentEnrollment, ObserverEnrollment].include?(@enrollment.class) && @context.grants_right?(@current_user, session, :manage_students)
can_move ||= @context.grants_right?(@current_user, session, :manage_admin_users)
can_move &&= @context.grants_right?(@current_user, session, :manage_account_settings) if @enrollment.defined_by_sis?
if can_move
respond_to do |format|
if @enrollment.defined_by_sis? &&! @context.grants_right?(@current_user, session, :manage_account_settings)
return format.json { render :json => @enrollment.to_json, :status => :bad_request }
end
@enrollment.course_section = @context.course_sections.find(params[:course_section_id])
@enrollment.save!
# ensure user_id,section_id,type,associated_user_id is unique (this
# will become a DB constraint eventually)
@possible_dup = @context.enrollments.find(:first, :conditions =>
["id <> ? AND user_id = ? AND course_section_id = ? AND type = ? AND (associated_user_id IS NULL OR associated_user_id = ?)",
@enrollment.id, @enrollment.user_id, params[:course_section_id], @enrollment.type, @enrollment.associated_user_id])
if @possible_dup.present?
format.json { render :json => @enrollment.to_json, :status => :forbidden }
else
@enrollment.course_section = @context.course_sections.find(params[:course_section_id])
@enrollment.save!
format.json { render :json => @enrollment.to_json }
format.json { render :json => @enrollment.to_json }
end
end
else
authorized_action(@context, @current_user, :permission_fail)

View File

@ -2,7 +2,8 @@
<% include_type ||= false; show_information_link ||= false %>
<% enrollments = enrollment[1].sort_by(&:id) if enrollment.is_a?(Array) %>
<% enrollment = enrollment[1][0] if enrollment.is_a?(Array) %>
<li id="enrollment_<%= enrollment.try_rescue(:id) || "blank" %>" class="user <%= "pending" if enrollment.try_rescue(:pending?) %> <%= enrollment.class.to_s.underscore %> user_<%= enrollment.try_rescue(:user_id) %>"
<% multiple_enrollments = enrollments && enrollments.length > 1 %>
<li id="enrollment_<%= enrollment.try_rescue(:id) || "blank" %>" class="user <%= "pending" if enrollment.try_rescue(:pending?) %> <%= enrollment.class.to_s.underscore %> user_<%= enrollment.try_rescue(:user_id) %>"
<% if enrollment.try_rescue(:user) %>
title="<%= enrollment.user.name %>: <%= enrollment.user.email %>"
<% end %>>
@ -25,8 +26,8 @@
<a href="#" class="edit_section_link no-hover"><%= image_tag "edit.png" %></a>
<% end %>
<% end %>
<% if allowedbypermissions %>
<a href="<%= context_url(@context, :context_unenroll_url, (enrollment.try_rescue(:id) || "{{ id }}" ) ) %>" class="unenroll_user_link <%= cant_unenroll %> no-hover" title="<%= t 'links.unenroll_user', "Remove User from Course" %>"><%= image_tag unenroll_image %></a>
<% if allowedbypermissions && !multiple_enrollments %>
<a href="<%= context_url(@context, :context_unenroll_url, (enrollment.try_rescue(:id) || "{{ id }}" ) ) %>" class="unenroll_user_link <%= cant_unenroll %> no-hover" title="<%= t 'links.unenroll_user_course', "Remove User from Course" %>"><%= image_tag unenroll_image %></a>
<% end %>
<a href="<%= re_send_confirmation_url( enrollment.try_rescue(:user_id) || "{{ user_id }}", enrollment.try_rescue(:user).try_rescue(:communication_channel).try_rescue(:id) || "{{ communication_channel_id }}", :enrollment_id => enrollment.try_rescue(:id) || "{{ id }}" ) rescue "#" %>" class="re_send_confirmation_url" style="display: none;">&nbsp;</a>
</span>
@ -81,20 +82,27 @@
</div>
<% end %>
<% if show_section && enrollments %>
<div class="sections">
<% enrollments.each do |enrollment| %>
<% if enrollment && enrollment.course_section %>
<div class="section"><%= enrollment.course_section.display_name %></div>
<% unless lockedbysis %>
<% form_tag course_move_enrollment_url(@context, enrollment.id), {:class => "enrollment_course_section_form", :style => "display: none;"} do %>
<select title="<%= t 'titles.course_section', "Course Section" %>" name="course_section_id" id="course_section_id">
<% @context.course_sections.active.each do |section| %>
<option value="<%= section.id %>" class="option_for_section_<%= section.id %>" <%= "selected=selected" if enrollment.course_section_id==section.id %>><%= section.display_name %></option>
<div class="section section_<%= enrollment.course_section_id %>">
<div class="section_name"><%= enrollment.course_section.display_name %></div>
<% unless lockedbysis %>
<% form_tag course_move_enrollment_url(@context, enrollment.id), {:class => "enrollment_course_section_form", :style => "display: none;"} do %>
<select title="<%= t 'titles.course_section', "Course Section" %>" name="course_section_id" class="course_section_id">
<% @context.course_sections.active.each do |section| %>
<option value="<%= section.id %>" class="option_for_section_<%= section.id %>" <%= "selected=selected" if enrollment.course_section_id==section.id %>><%= section.display_name %></option>
<% end %>
</select>
<% if allowedbypermissions && multiple_enrollments %>
<a href="<%= context_url(@context, :context_unenroll_url, (enrollment.try_rescue(:id) || "{{ id }}" ) ) %>" class="unenroll_user_link <%= cant_unenroll %> no-hover" title="<%= t 'links.unenroll_user_section', "Remove User from Section" %>"><%= image_tag unenroll_image %></a>
<% end %>
</select>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<span class="invitation_sent_at" style="display: none;"><%= datetime_string(enrollment.try_rescue(:updated_at)) || nbsp %></span>
<span class="id" style="display: none;"><%= enrollment.try_rescue(:id) %></span>

View File

@ -439,19 +439,24 @@ require([
$('.user_list .edit_section_link').click(function(event) {
event.preventDefault();
var $this = $(this);
$user = $this.closest('.user');
$user.find('.section').toggle();
$user.find('.enrollment_course_section_form').toggle();
var $user = $this.parents('.user');
var $sections = $user.find('.sections');
$sections.find('.section_name').toggle();
$sections.find('.enrollment_course_section_form').toggle();
});
$('.user_list .enrollment_course_section_form #course_section_id').change(function (event) {
var form = $(this).parent('form');
var $this = $(this)
var section = form.prev('.section')
$.ajaxJSON(form.attr('action'), 'POST', form.getFormData(), function(data) {
section.html($this.find('option[value="' + $this.val() + '"]').html());
section.toggle();
form.toggle();
$('.user_list .enrollment_course_section_form .course_section_id').change(function (event) {
var $this = $(this);
var $sections = $this.parents('.sections');
var $form = $this.parent('form');
var $section_name = $form.prev('.section_name');
$.ajaxJSON($form.attr('action'), 'POST', $form.getFormData(), function(data) {
$section_name.html($this.find('option[value="' + $this.val() + '"]').html());
$sections.find('.section_name').toggle();
$sections.find('.enrollment_course_section_form').toggle();
}, function(data) {
if (data && data.enrollment) {
$this.val(data.enrollment.course_section_id);
}
$.flashError(I18n.t('errors.move_user', "Something went wrong moving the user to the new section. Please try again later."));
});
});

View File

@ -70,10 +70,16 @@ require([
$form.find(".user_list").val("");
UL.showTextarea();
if (!enrollments || !enrollments.length) { return false; }
var already_existed = 0;
$.each( enrollments, function(){
UL.addUserToList(this.enrollment);
already_existed += UL.addUserToList(this.enrollment);
});
$.flashMessage(I18n.t("users_added", { one: "1 user added", other: "%{count} users added" }, { count: enrollments.length }));
var addedMsg = I18n.t("users_added", { one: "1 user added", other: "%{count} users added" }, { count: enrollments.length - already_existed });
if (already_existed > 0) {
addedMsg += " " + I18n.t("users_existed", { one: "(1 user already existed)", other: "(%{count} users already existed)" }, { count: already_existed });
}
$.flashMessage(addedMsg);
}, function(data) {
$.flashError(I18n.t("users_adding_failed", "Failed to enroll users"));
});
@ -88,7 +94,14 @@ require([
if($(this).hasClass('cant_unenroll')) {
alert(I18n.t("cant_unenroll", "This user was automatically enrolled using the campus enrollment system, so they can't be manually removed. Please contact your system administrator if you have questions."));
} else {
$(this).parents(".user").confirmDelete({
$user = $(this).parents('.user')
$sections = $(this).parents('.sections')
$section = $(this).parents('.section')
var $toDelete = $user;
if ($sections.find('.section:visible').size() > 1) {
$toDelete = $section;
}
$toDelete.confirmDelete({
message: I18n.t("delete_confirm", "Are you sure you want to remove this user?"),
url: $(this).attr('href'),
success: function() {
@ -175,7 +188,10 @@ require([
return false;
}
});
var already_existed = true;
if(!$("#enrollment_" + enrollment.id).length) {
already_existed = false;
var $enrollment = $enrollment_blank
.clone(true)
.fillTemplateData({
@ -200,6 +216,7 @@ require([
.scrollToVisible($enrollment);
}
UL.updateCounts();
return already_existed ? 1 : 0;
}
};

View File

@ -8,23 +8,13 @@ describe "course settings tests" do
end
def add_section(section_name)
@course.course_sections.create!(:name => section_name)
@course_section = @course.course_sections.create!(:name => section_name)
@course.reload
end
def add_user_to_section(username = 'user@example.com', accept_invitation = true)
cs = @course.course_sections.create!
u = User.create!(:name => username)
u.register!
if accept_invitation
@course.enroll_user(u, 'StudentEnrollment', :section => cs).accept
else
e = @course.enroll_user(u, 'StudentEnrollment', :section => cs)
e.workflow_state = 'active'
e.save!
end
@course.reload
username
def multiple_student_enrollment
@enrollment = @course.student_enrollments.build(:user => @user, :workflow_state => "active", :course_section => @course_section)
@enrollment.save!
end
describe "course items" do
@ -137,36 +127,37 @@ describe "course settings tests" do
end
it "should remove a user from a section" do
username = add_user_to_section
username = "user@example.com"
student_in_course_section(:name => username)
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('li.student_enrollment .unenroll_user_link').click()")
driver.execute_script("$('#enrollment_#{@enrollment.id} .unenroll_user_link').click()")
driver.switch_to.alert.accept
keep_trying_until do
driver.find_element(:id, 'tab-users').should_not include_text(username)
true
end
wait_for_ajaximations
driver.find_element(:id, 'tab-users').should_not include_text(username)
end
it "should move a user to a new section" do
section_name = 'Unnamed Course'
add_user_to_section
section_name = 'Move to Course Section'
add_section(section_name)
student_in_course_section(:course_section => @course_section)
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('li.student_enrollment .edit_section_link').click()")
click_option('#course_section_id', section_name)
driver.execute_script("$('#enrollment_#{@enrollment.id} .edit_section_link').click()")
click_option("#enrollment_#{@enrollment.id} .course_section_id", section_name)
wait_for_ajaximations
driver.find_element(:css, 'li.student_enrollment .section').should include_text(section_name)
driver.find_element(:css, "#enrollment_#{@enrollment.id} .section").should include_text(section_name)
end
it "should view the users enrollment details" do
username = add_user_to_section('user@example.com', true)
username = "user@example.com"
student_in_course_section(:name => username)
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('li.student_enrollment .user_information_link').click()")
driver.execute_script("$('#enrollment_#{@enrollment.id} .user_information_link').click()")
enrollment_dialog = driver.find_element(:id, 'enrollment_dialog')
enrollment_dialog.should be_displayed
enrollment_dialog.should include_text(username + ' has already received and accepted the invitation')
@ -229,4 +220,56 @@ describe "course settings tests" do
@obs.enrollments.map {|e| e.associated_user_id}.sort.should == [@students[0].id, @students[1].id]
end
end
describe "course users multiple enrollments" do
before (:each) do
@username = "multiple@example.com"
add_section("Section 1")
@old_section = @course_section
student_in_course_section(:name => @username, :course_section => @course_section)
add_section("Section 2")
multiple_student_enrollment
end
it "should coalesce multiple enrollments under a single student" do
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.find_elements(:css, ".user_#{@user.id} .section").length.should == 2
driver.find_elements(:css, ".user_#{@user.id} .links .unenroll_user_link").length.should == 0
end
it "should show individual course section remove icons" do
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('.user_#{@user.id} .edit_section_link').click()")
find_all_with_jquery(".user_#{@user.id} .sections .unenroll_user_link:visible").length.should == 2
end
it "should only remove a user from a single section" do
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('.user_#{@user.id} .section_#{@course_section.id} .unenroll_user_link').click()")
driver.switch_to.alert.accept
wait_for_ajaximations
driver.find_element(:id, 'tab-users').should include_text(@username)
driver.find_element(:css, ".user_#{@user.id}").should include_text(@old_section.name)
end
it "should change the correct section when editing" do
add_section("Section 3")
get "/courses/#{@course.id}/settings"
driver.find_element(:link, 'Users').click
driver.execute_script("$('.user_#{@user.id} .edit_section_link').click()")
click_option(".user_#{@user.id} .section_#{@old_section.id} .course_section_id", @course_section.name)
wait_for_ajaximations
driver.find_element(:css, ".user_#{@user.id}").should_not include_text(@old_section.name)
driver.find_element(:css, ".user_#{@user.id}").should include_text(@course_section.name)
end
end
end

View File

@ -224,4 +224,4 @@ describe "course wizard" do
get "/"
driver.find_elements(:link, 'Start a New Course').should be_empty
end
end
end

View File

@ -239,8 +239,8 @@ describe "courses" do
enrollment = user[:enrollment]
enrollment_element = driver.find_element(:css, "#enrollment_#{enrollment.id}")
section_label = enrollment_element.find_element(:css, ".section") rescue nil
section_dropdown = enrollment_element.find_element(:css, ".enrollment_course_section_form #course_section_id") rescue nil
section_label = enrollment_element.find_element(:css, ".section_name") rescue nil
section_dropdown = enrollment_element.find_element(:css, ".enrollment_course_section_form .course_section_id") rescue nil
edit_section_link = enrollment_element.find_element(:css, ".edit_section_link") rescue nil
unenroll_user_link = enrollment_element.find_element(:css, ".unenroll_user_link") rescue nil

View File

@ -281,6 +281,17 @@ Spec::Runner.configure do |config|
course_with_student(opts)
end
def student_in_course_section(opts={})
@course ||= opts[:course] || course(opts)
@course_section = opts[:course_section] || @course.course_sections.create!
@user = opts[:user] || user(opts)
@user.register!
@enrollment = @course.enroll_user(@user, 'StudentEnrollment', :section => @course_section)
@enrollment.workflow_state = 'active'
@enrollment.save!
@course.reload
end
def course_with_teacher(opts={})
course_with_user('TeacherEnrollment', opts)
@teacher = @user