Extract Soft Deletion

fixes CNVS-20361

This is the first step in extracting soft deletion and
only touches grading period models. I audited other models
to see if this extraction would work for them, but no other
models met my criteria which are as follows

- [ ] defines workflow state of _only_ active and deleted
- [ ] active scope where state is active
      (not `workflow_state<>'deleted'`)
In `def destroy`:
  - [ ] state set to deleted
  - [ ] then save
  - [ ] runs callbacks

Many variants of soft deletion exist, including scoping
active to `workflow_state<>'deleted'`, not running
callbacks after save, and wrapping the state change in a
transaction block.

This is the first step in making soft deletion behave
consistently. There are approximately 30 models that have
an implementation of soft deletion.

Change-Id: I6cb48571377a4bb403285f95c058020b46ca3a30
Reviewed-on: https://gerrit.instructure.com/53821
Reviewed-by: Derek Bender <dbender@instructure.com>
Product-Review: Strand McCutchen <smccutchen@instructure.com>
QA-Review: Strand McCutchen <smccutchen@instructure.com>
Tested-by: Jenkins
This commit is contained in:
Strand McCutchen 2015-05-08 16:38:39 -07:00
parent d2305d64dd
commit 05f4b9190e
9 changed files with 137 additions and 68 deletions

View File

@ -1,5 +1,5 @@
class GradingPeriod < ActiveRecord::Base
include Workflow
include Canvas::SoftDeletable
attr_accessible :weight, :start_date, :end_date, :title
@ -17,12 +17,6 @@ class GradingPeriod < ActiveRecord::Base
can :manage
end
workflow do
state :active
state :deleted
end
scope :active, -> { where workflow_state: "active" }
scope :current, -> { where("start_date <= ? AND end_date >= ?", Time.now, Time.now) }
scope :grading_periods_by, ->(context_with_ids) {
joins(:grading_period_group).where(grading_period_groups: context_with_ids)
@ -47,16 +41,6 @@ class GradingPeriod < ActiveRecord::Base
start_date <= Time.now && end_date >= Time.now
end
# save the previous definition of `destroy` and alias it to `destroy!`
# Note: `destroy!` now does NOT throw errors while the newly defined
# `destroy` DOES throw errors due to `save!`
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
save!
run_callbacks :destroy
end
def assignments(assignment_scope)
# TODO: avoid wasteful queries
assignments = assignment_scope.where( "due_at BETWEEN ? AND ?", start_date, end_date)

View File

@ -1,5 +1,5 @@
class GradingPeriodGrade < ActiveRecord::Base
include Workflow
include Canvas::SoftDeletable
#TODO: when we create a controller for this, remove attr_accessible and use strong params instead
attr_accessible :enrollment_id, :grading_period_id, :current_grade, :final_grade
@ -15,17 +15,4 @@ class GradingPeriodGrade < ActiveRecord::Base
can permission
end
end
workflow do
state :active
state :deleted
end
scope :active, -> { where workflow_state: "active" }
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
save!
end
end

View File

@ -1,8 +1,7 @@
class GradingPeriodGroup < ActiveRecord::Base
include Workflow
attr_accessible
include Canvas::SoftDeletable
attr_accessible # None of this model's attributes are mass-assignable
belongs_to :course
belongs_to :account
has_many :grading_periods, dependent: :destroy
@ -20,24 +19,10 @@ class GradingPeriodGroup < ActiveRecord::Base
can :manage
end
workflow do
state :active
state :deleted
end
scope :active, -> { where workflow_state: "active" }
def multiple_grading_periods_enabled?
(course || account).feature_enabled?(:multiple_grading_periods) || account_grading_period_allowed?
end
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
save!
run_callbacks :destroy
end
private
def belongs_to_course_or_account_exclusive
if course.blank? && account.blank?

View File

@ -0,0 +1,26 @@
require 'active_support/concern'
module Canvas::SoftDeletable
extend ActiveSupport::Concern
included do
include Workflow
workflow do
state :active
state :deleted
end
scope :active, -> { where workflow_state: "active" }
# save the previous definition of `destroy` and alias it to `destroy!`
# Note: `destroy!` now does NOT throw errors while the newly defined
# `destroy` DOES throw errors due to `save!`
alias_method :destroy!, :destroy
def destroy
self.workflow_state = 'deleted'
save!
run_callbacks :destroy
end
end
end

View File

@ -0,0 +1,36 @@
#
# Copyright (C) 2014 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 File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
describe GradingPeriodGrade do
context "Soft deletion" do
let(:student) { User.create.student_enrollments.create course: course }
let(:account) { Account.create }
let(:group) { account.grading_period_groups.create }
let(:period) do
group.grading_periods.create start_date: 1.week.ago,
end_date: 2.weeks.from_now
end
let(:creation_arguments) { { grading_period_id: period.id } }
subject { student.grading_period_grades }
include_examples "soft deletion"
end
end

View File

@ -86,27 +86,25 @@ describe GradingPeriodGroup do
end
end
describe "#destroy" do
let(:grading_period_group) { Account.default.grading_period_groups.create }
context "Soft deletion" do
let(:account) { Account.create }
let(:creation_arguments) { {} }
subject { account.grading_period_groups }
include_examples "soft deletion"
it "should soft delete by setting the workflow status to deleted" do
expect(grading_period_group).to_not be_deleted
grading_period_group.destroy
expect(grading_period_group).to be_deleted
expect(grading_period_group).to_not be_destroyed
end
describe "#destroy" do
let(:grading_period_group) { Account.default.grading_period_groups.create }
it "should soft delete grading periods that belong to the grading period group when it is destroyed" do
grading_period_1 = grading_period_group.grading_periods.create!(start_date: Time.now, end_date: 2.months.from_now)
grading_period_2 = grading_period_group.grading_periods.create!(start_date: 2.months.from_now, end_date: 3.months.from_now)
expect(grading_period_group).to_not be_deleted
expect(grading_period_1).to_not be_deleted
expect(grading_period_2).to_not be_deleted
grading_period_group.destroy
expect(grading_period_1).to be_deleted
expect(grading_period_1).to_not be_destroyed
expect(grading_period_2).to be_deleted
expect(grading_period_2).to_not be_destroyed
it "should soft delete grading periods that belong to the grading period group when it is destroyed" do
grading_period_1 = grading_period_group.grading_periods.create!(start_date: Time.now, end_date: 2.months.from_now)
grading_period_2 = grading_period_group.grading_periods.create!(start_date: 2.months.from_now, end_date: 3.months.from_now)
grading_period_group.destroy
expect(grading_period_1).to be_deleted
expect(grading_period_1).to_not be_destroyed
expect(grading_period_2).to be_deleted
expect(grading_period_2).to_not be_destroyed
end
end
end

View File

@ -137,5 +137,13 @@ describe GradingPeriod do
expect(grading_period.current?).to be false
end
end
context "Soft deletion" do
let(:account) { Account.create }
let(:group) { account.grading_period_groups.create }
let(:creation_arguments) { { start_date: 1.week.ago, end_date: 2.weeks.from_now } }
subject { group.grading_periods }
include_examples "soft deletion"
end
end

View File

@ -0,0 +1,41 @@
shared_examples "soft deletion" do
let(:first) { subject.create creation_arguments }
let(:second) { subject.create creation_arguments }
let(:active_scope) { subject.active }
describe "workflow" do
it "defaults to active" do
expect(first.active?).to be true
end
it "is deleted after destroy is called" do
first.destroy
expect(first.deleted?).to be true
end
end
describe "#active" do
let!(:destroy_the_second_active_object) { second.destroy }
it "includes active grading_periods" do
expect(active_scope).to include first
end
it "does not include inactive grading_periods" do
expect(active_scope).to_not include second
end
end
describe "#destroy" do
it "marks deleted periods workflow_state as deleted" do
first.destroy
expect(first.workflow_state).to eq "deleted"
end
# Use Mocha to test this.
it "calls save"
it "calls save! if destroy! was called"
it "triggers destroy callbacks"
end
end

View File

@ -165,7 +165,11 @@ Mocha::ObjectMethods.instance_methods.each do |m|
RUBY
end
Dir.glob("#{File.dirname(__FILE__).gsub(/\\/, "/")}/factories/*.rb").each { |file| require file }
factories = "#{File.dirname(__FILE__).gsub(/\\/, "/")}/factories/*.rb"
Dir.glob(factories).each { |file| require file }
examples = "#{File.dirname(__FILE__).gsub(/\\/, "/")}/shared_examples/*.rb"
Dir.glob(examples).each { |file| require file }
def pend_with_bullet
if defined?(Bullet) && Bullet.enable?