fix course[event] in "Update a course" API

test plan:
 - Consult the API docs for "Update a course"
   (/doc/api/courses.html#method.courses.update)
 - ensure the course[event] parameter can be used as
   documented (claim, offer, conclude, delete, undelete)
 - ensure a teacher can do all of these except undelete
   which requires account admin rights
 - ensure if an invalid course[event] is supplied,
   a sensible error is returned
 - ensure if an invalid transition is attempted
   (e.g. trying to publish a deleted course without
   undeleting it first), a sensible error is returned

fixes ADMIN-406

Change-Id: Ie2ea4ab13d384bfd226fd40e8115c3570213b5d7
Reviewed-on: https://gerrit.instructure.com/184959
Tested-by: Jenkins
Reviewed-by: Mysti Lilla <mysti@instructure.com>
QA-Review: Mysti Lilla <mysti@instructure.com>
Product-Review: Mysti Lilla <mysti@instructure.com>
This commit is contained in:
Jeremy Stanley 2019-03-11 16:20:36 -06:00
parent c013ee4396
commit 8658871eec
3 changed files with 145 additions and 22 deletions

View File

@ -2239,9 +2239,9 @@ class CoursesController < ApplicationController
# in prior-enrollment lists.
# * 'delete' completely removes the course from the web site (including course menus and prior-enrollment lists).
# All enrollments are deleted. Course content may be physically deleted at a future date.
# * 'undelete' attempts to recover a course that has been deleted. (Recovery is not guaranteed; please conclude
# rather than delete a course if there is any possibility the course will be used again.) The recovered course
# will be unpublished. Deleted enrollments will not be recovered.
# * 'undelete' attempts to recover a course that has been deleted. This action requires account administrative rights.
# (Recovery is not guaranteed; please conclude rather than delete a course if there is any possibility the course
# will be used again.) The recovered course will be unpublished. Deleted enrollments will not be recovered.
#
# @argument course[default_view] [String, "feed"|"wiki"|"modules"|"syllabus"|"assignments"]
# The type of page that users will see when they first visit the course
@ -2321,8 +2321,12 @@ class CoursesController < ApplicationController
params[:course][:event] = :offer if params[:offer].present?
if params[:course][:event] && params[:course].keys.size == 1
if authorized_action(@course, @current_user, :change_course_state) && process_course_event
if authorized_action(@course, @current_user, :change_course_state)
if process_course_event
render_update_success
else
render_update_failure
end
end
return
end
@ -2421,7 +2425,10 @@ class CoursesController < ApplicationController
end
if params[:course][:event] && @course.grants_right?(@current_user, session, :change_course_state)
return unless process_course_event
unless process_course_event
render_update_failure
return
end
end
if params[:course][:image_url] && params[:course][:image_id]
@ -2516,6 +2523,12 @@ class CoursesController < ApplicationController
end
render_update_success
else
render_update_failure
end
end
end
def render_update_failure
respond_to do |format|
format.html do
flash[:error] = t('There was an error saving the changes to the course')
@ -2524,24 +2537,60 @@ class CoursesController < ApplicationController
format.json { render :json => @course.errors, :status => :bad_request }
end
end
# prevent API from failing when a no-op event is given
def non_event?(course, event)
case event
when :offer
course.available?
when :claim
course.claimed?
when :complete
course.completed?
when :delete
course.deleted?
else
false
end
end
def process_course_event
event = params[:course].delete(:event)
event = event.to_sym
event = :complete if event == :conclude
return true if non_event?(@course, event)
if event == :claim && !@course.unpublishable?
flash[:error] = t('errors.unpublish', 'Course cannot be unpublished if student submissions exist.')
cant_unpublish_message = t('errors.unpublish', 'Course cannot be unpublished if student submissions exist.')
respond_to do |format|
format.json do
@course.errors.add(:workflow_state, cant_unpublish_message)
end
format.html do
flash[:error] = cant_unpublish_message
redirect_to(course_url(@course))
end
end
return false
else
@course.process_event(event)
logging_source = api_request? ? :api : :manual
if event == :offer
Auditors::Course.record_published(@course, @current_user, source: logging_source)
elsif event == :claim
Auditors::Course.record_claimed(@course, @current_user, source: logging_source)
result = @course.process_event(event)
if result
opts = { source: api_request? ? :api : :manual }
case event
when :offer
Auditors::Course.record_published(@course, @current_user, opts)
when :claim
Auditors::Course.record_claimed(@course, @current_user, opts)
when :complete
Auditors::Course.record_concluded(@course, @current_user, opts)
when :delete
Auditors::Course.record_deleted(@course, @current_user, opts)
when :undelete
Auditors::Course.record_restored(@course, @current_user, opts)
end
else
@course.errors.add(:workflow_state, @course.halted_because)
end
result
end
end

View File

@ -1263,22 +1263,31 @@ class Course < ActiveRecord::Base
event :claim, :transitions_to => :claimed
event :offer, :transitions_to => :available
event :complete, :transitions_to => :completed
event :delete, :transitions_to => :deleted
end
state :claimed do
event :offer, :transitions_to => :available
event :complete, :transitions_to => :completed
event :delete, :transitions_to => :deleted
end
state :available do
event :complete, :transitions_to => :completed
event :claim, :transitions_to => :claimed
event :delete, :transitions_to => :deleted
end
state :completed do
event :unconclude, :transitions_to => :available
event :offer, :transitions_to => :available
event :claim, :transitions_to => :claimed
event :delete, :transitions_to => :deleted
end
state :deleted do
event :undelete, :transitions_to => :claimed
end
state :deleted
end
def api_state

View File

@ -1522,11 +1522,18 @@ describe CoursesController do
end
it "should log published event on update" do
@course.claim!
expect(Auditors::Course).to receive(:record_published).once
user_session(@teacher)
put 'update', params: {:id => @course.id, :offer => true}
end
it "should not log published event if course was already published" do
expect(Auditors::Course).to receive(:record_published).never
user_session(@teacher)
put 'update', params: {:id => @course.id, :offer => true}
end
it "should log claimed event on update" do
expect(Auditors::Course).to receive(:record_claimed).once
user_session(@teacher)
@ -1564,6 +1571,64 @@ describe CoursesController do
expect(@course.workflow_state).to eq 'claimed'
end
it "concludes a course" do
expect(Auditors::Course).to receive(:record_concluded).once
user_session(@teacher)
put 'update', params: {:id => @course.id, :course => {:event => "conclude"}, :format => :json}
json = JSON.parse response.body
expect(json['course']['workflow_state']).to eq 'completed'
@course.reload
expect(@course.workflow_state).to eq 'completed'
end
it "publishes a course" do
@course.claim!
expect(Auditors::Course).to receive(:record_published).once
user_session(@teacher)
put 'update', params: {:id => @course.id, :course => {:event => 'offer'}, :format => :json}
json = JSON.parse response.body
expect(json['course']['workflow_state']).to eq 'available'
@course.reload
expect(@course.workflow_state).to eq 'available'
end
it "deletes a course" do
user_session(@teacher)
expect(Auditors::Course).to receive(:record_deleted).once
put 'update', params: {:id => @course.id, :course => {:event => 'delete'}, :format => :json}
json = JSON.parse response.body
expect(json['course']['workflow_state']).to eq 'deleted'
@course.reload
expect(@course.workflow_state).to eq 'deleted'
end
it "doesn't allow a teacher to undelete a course" do
@course.destroy
expect(Auditors::Course).to receive(:record_restored).never
user_session(@teacher)
put 'update', params: {:id => @course.id, :course => {:event => 'undelete'}, :format => :json}
expect(response.status).to eq 401
end
it "undeletes a course" do
@course.destroy
expect(Auditors::Course).to receive(:record_restored).once
user_session(account_admin_user)
put 'update', params: {:id => @course.id, :course => {:event => 'undelete'}, :format => :json}
json = JSON.parse response.body
expect(json['course']['workflow_state']).to eq 'claimed'
@course.reload
expect(@course.workflow_state).to eq 'claimed'
end
it "returns an error if a bad event is given" do
user_session(@teacher)
put 'update', params: {:id => @course.id, :course => {:event => 'boogie'}, :format => :json}
expect(response.status).to eq 400
json = JSON.parse response.body
expect(json['errors'].keys).to include 'workflow_state'
end
it "should lock active course announcements" do
user_session(@teacher)
active_announcement = @course.announcements.create!(:title => 'active', :message => 'test')