diff --git a/app/controllers/outcome_results_controller.rb b/app/controllers/outcome_results_controller.rb index fa14846a031..ff965bdd26d 100644 --- a/app/controllers/outcome_results_controller.rb +++ b/app/controllers/outcome_results_controller.rb @@ -371,6 +371,7 @@ class OutcomeResultsController < ApplicationController def find_outcomes_service_results(opts = {}) find_outcomes_service_outcome_results( + @current_user, users: opts[:all_users] ? @all_users : @users, context: @context, outcomes: @outcomes, diff --git a/app/helpers/outcome_result_resolver_helper.rb b/app/helpers/outcome_result_resolver_helper.rb index e4da256c7e8..c2ec6d6e66f 100644 --- a/app/helpers/outcome_result_resolver_helper.rb +++ b/app/helpers/outcome_result_resolver_helper.rb @@ -21,11 +21,7 @@ module OutcomeResultResolverHelper include OutcomesServiceAuthoritativeResultsHelper def resolve_outcome_results(authoritative_results) - # TODO: Since get_lmgb_results returns a parsed JSON object - # we will need to update json_to_outcome_results to handle an already parsed object - # this will be done in a separate PS as it is not just a simple fix since this helper - # module is heavily dependent on the data to be in JSON. See OUT-5283 - results = json_to_outcome_results({ results: authoritative_results }.to_json) + results = convert_to_learning_outcome_results(authoritative_results) rubric_results = LearningOutcomeResult.preload(:learning_outcome).active.where(association_type: "RubricAssociation") results.reject { |res| rubric_result?(res, rubric_results) } end diff --git a/app/helpers/outcomes_service_authoritative_results_helper.rb b/app/helpers/outcomes_service_authoritative_results_helper.rb index aff592d2576..194a5b5e964 100644 --- a/app/helpers/outcomes_service_authoritative_results_helper.rb +++ b/app/helpers/outcomes_service_authoritative_results_helper.rb @@ -17,39 +17,40 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . -# This helper provides methods to transform JSON data retrieved upon calling +# This helper provides methods to transform hash data retrieved upon calling # OutcomesService (OS) authoritative_results endpoint either into: # # - a RollupScores collection via Outcomes::ResultAnalytics # - a LearningOutcomeResult collection # # It also provides a transformation method for a single OS' AuthoritativeResult -# JSON object into a LearningOutcomeResult object +# hash object into a LearningOutcomeResult object module OutcomesServiceAuthoritativeResultsHelper Rollup = Struct.new(:context, :scores) - # Transforms an OS' JSON AuthoritativeResult collection into a + # Transforms an OS' hash of AuthoritativeResult collection into a # RollupScore collection - def json_to_rollup_scores(authoritative_results) - rollup_user_results json_to_outcome_results(authoritative_results) + def rollup_scores(authoritative_results) + rollup_user_results convert_to_learning_outcome_results(authoritative_results) end - # Transforms an OS' JSON AuthoritativeResult collection into a + # Transforms an OS' hash AuthoritativeResult collection into a # LearningOutcomeResult collection - def json_to_outcome_results(authoritative_results) - JSON.parse(authoritative_results).deep_symbolize_keys[:results].map do |r| - json_to_outcome_result(r) + def convert_to_learning_outcome_results(authoritative_results) + authoritative_results.each_with_object([]) do |r, all_results| + result = convert_to_learning_outcome_result(r) + all_results.push(result) unless result.nil? end end - # Transforms an OS' JSON AuthoritativeResult (AR) object into an + # Transforms an OS' hash AuthoritativeResult (AR) object into an # instance of LearningOutcomeResult - def json_to_outcome_result(authoritative_result) + def convert_to_learning_outcome_result(authoritative_result) outcome = LearningOutcome.find(authoritative_result[:external_outcome_id]) assignment = Assignment.find(authoritative_result[:associated_asset_id]) - user = User.find_by(uuid: authoritative_result[:user_uuid]) - submission = Submission.find_by(user_id: user.id, assignment_id: assignment.id) + student_user = User.find_by(uuid: authoritative_result[:user_uuid]) + submission = Submission.find_by(user_id: student_user.id, assignment_id: assignment.id) context = assignment.context root_account = assignment.root_account @@ -75,9 +76,9 @@ module OutcomesServiceAuthoritativeResultsHelper learning_outcome: outcome, associated_asset: assignment, artifact: submission, - title: "#{user.name}, #{assignment.name}", - user: user, - user_uuid: user.uuid, + title: "#{student_user.name}, #{assignment.name}", + user: student_user, + user_uuid: student_user.uuid, alignment: alignment, context: context, root_account: root_account, diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 7f9c428e951..3b9473cc2e5 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -162,6 +162,16 @@ class Assignment < ActiveRecord::Base } scope :not_type_quiz_lti, -> { where.not(id: type_quiz_lti) } + scope :exclude_muted_associations_for_user, lambda { |user| + joins("LEFT JOIN #{Submission.quoted_table_name} ON submissions.user_id = #{User.connection.quote(user.id_for_database)} AND submissions.assignment_id = assignments.id") + .joins("LEFT JOIN #{PostPolicy.quoted_table_name} pc on pc.assignment_id = assignments.id") + .where(" assignments.id IS NULL"\ + " OR submissions.posted_at IS NOT NULL"\ + " OR assignments.grading_type = 'not_graded'"\ + " OR pc.id IS NULL"\ + " OR (pc.id IS NOT NULL AND pc.post_manually = False)") + } + validates_associated :external_tool_tag, if: :external_tool? validate :group_category_changes_ok? validate :turnitin_changes_ok? diff --git a/gems/plugins/account_reports/lib/account_reports/outcome_reports.rb b/gems/plugins/account_reports/lib/account_reports/outcome_reports.rb index 3064cc33a89..2ce0b7b789a 100644 --- a/gems/plugins/account_reports/lib/account_reports/outcome_reports.rb +++ b/gems/plugins/account_reports/lib/account_reports/outcome_reports.rb @@ -302,7 +302,7 @@ module AccountReports end def combine_result(student, authoritative_results) - learning_outcome_result = json_to_outcome_result(authoritative_results) + learning_outcome_result = convert_to_learning_outcome_result(authoritative_results) base_student = student.attributes.merge( { "learning outcome mastered" => learning_outcome_result.mastery, diff --git a/lib/outcomes/result_analytics.rb b/lib/outcomes/result_analytics.rb index 032cf408f4a..46f6b5c12e2 100644 --- a/lib/outcomes/result_analytics.rb +++ b/lib/outcomes/result_analytics.rb @@ -45,10 +45,11 @@ module Outcomes learning_outcome_id: outcomes.map(&:id) ) # muted associations is applied to remove assignments that students - # are not yet allowed to see: + # are not yet allowed to view: # Assignment Grades have not be posted yet (i.e. Submission.posted_at = nil) - # PostPolicy.post_manually is false or null + # PostPolicy.post_manually is false & the submission is not posted # Assignment grading_type is not_graded + # see result_analytics_spec.rb for more details around what is excluded/included unless context.grants_any_right?(user, :manage_grades, :view_all_grades) results = results.exclude_muted_associations end @@ -72,12 +73,25 @@ module Outcomes # :outcomes - The outcomes to lookup results for (required) # # Returns a relation of the results - def find_outcomes_service_outcome_results(opts) + def find_outcomes_service_outcome_results(user, opts) required_opts = %i[users context outcomes] required_opts.each { |p| raise "#{p} option is required" unless opts[p] } users, context, outcomes = opts.values_at(*required_opts) user_uuids = users.pluck(:uuid).join(",") - assignment_ids = Assignment.where(context: context).type_quiz_lti.pluck(:id).join(",") + + # check if the logged in user has manage_grades & view_all_grades permissions + # if not, apply exclude_muted_associations to the assignment query + assignment_ids = + if context.grants_any_right?(user, :manage_grades, :view_all_grades) + Assignment.active.where(context: context).quiz_lti.pluck(:id).join(",") + else + # return if there is more than one user in users as this would indicate + # user with insufficient permissions accessing the LMGB + return if users.length > 1 + + Assignment.active.where(context: context).quiz_lti.exclude_muted_associations_for_user(users[0]).pluck(:id).join(",") + end + outcome_ids = outcomes.pluck(:id).join(",") handle_outcome_service_results( get_lmgb_results(context, assignment_ids, "canvas.assignment.quizzes", outcome_ids, user_uuids), diff --git a/spec/factories/outcome_alignment_results_factory.rb b/spec/factories/outcome_alignment_results_factory.rb index e7b25733c23..08f62fd2662 100644 --- a/spec/factories/outcome_alignment_results_factory.rb +++ b/spec/factories/outcome_alignment_results_factory.rb @@ -96,24 +96,21 @@ module Factories # Mocks calls to the OS endpoints: # # - retrieving data from the Canvas' LearningOutcomeResult table - # - transforming this data into a collection of JSON AuthoritativeResult objects + # - transforming this data into a collection of AuthoritativeResult hash objects def authoritative_results_from_db - { - results: - LearningOutcomeResult.all.map do |lor| - { - user_uuid: lor.user.uuid, - points: lor.score, - points_possible: lor.possible, - external_outcome_id: lor.learning_outcome.id, - attempts: nil, - associated_asset_type: nil, - associated_asset_id: lor.alignment.content_id, - artifact_type: nil, - artifact_id: nil, - submitted_at: lor.submitted_at - } - end - }.to_json + LearningOutcomeResult.all.map do |lor| + { + user_uuid: lor.user.uuid, + points: lor.score, + points_possible: lor.possible, + external_outcome_id: lor.learning_outcome.id, + attempts: nil, + associated_asset_type: nil, + associated_asset_id: lor.alignment.content_id, + artifact_type: nil, + artifact_id: nil, + submitted_at: lor.submitted_at + } + end end end diff --git a/spec/helpers/outcome_result_resolver_helper_spec.rb b/spec/helpers/outcome_result_resolver_helper_spec.rb index be1e728f48d..f651b247fb6 100644 --- a/spec/helpers/outcome_result_resolver_helper_spec.rb +++ b/spec/helpers/outcome_result_resolver_helper_spec.rb @@ -35,18 +35,11 @@ describe OutcomeResultResolverHelper do end end - # TODO: authoritative_results_from_db to return a hash not json - # Since get_lmgb_results returns a parsed JSON object, - # we will need to update json_to_outcome_results, which is called in - # resolve_outcome_results, to handle an already parsed object. - # This will be done in a separate PS as it is not just a simple fix since this helper - # module is heavily dependent on the data to be in JSON. See OUT-5283 describe "removes the alignment result" do it "if there is a rubric result for that student, assignment, and outcome" do create_outcome create_alignment lor = create_learning_outcome_result @students[0], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] # We cannot have two LORs for the same student, assignment, and outcome in the db lor.workflow_state = "deleted" @@ -55,7 +48,7 @@ describe OutcomeResultResolverHelper do create_alignment_with_rubric({ assignment: @assignment }) create_learning_outcome_result_from_rubric @students[0], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 0 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 0 end it "if a rubric result exists for multiple students for the same assignment and outcome" do @@ -63,7 +56,6 @@ describe OutcomeResultResolverHelper do create_alignment lor1 = create_learning_outcome_result @students[0], 1.0 lor2 = create_learning_outcome_result @students[1], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] # We cannot have two LORs for the same student, assignment, and outcome in the db lor1.workflow_state = "deleted" @@ -75,7 +67,7 @@ describe OutcomeResultResolverHelper do create_learning_outcome_result_from_rubric @students[0], 1.0 create_learning_outcome_result_from_rubric @students[1], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 0 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 0 end it "for just one student if a rubric result exists for their assignment and outcome" do @@ -83,7 +75,6 @@ describe OutcomeResultResolverHelper do create_alignment lor = create_learning_outcome_result @students[0], 1.0 create_learning_outcome_result @students[1], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] # We cannot have two LORs for the same student, assignment, and outcome in the db lor.workflow_state = "deleted" @@ -92,7 +83,7 @@ describe OutcomeResultResolverHelper do create_alignment_with_rubric({ assignment: @assignment }) create_learning_outcome_result_from_rubric @students[0], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end it "for an assignment with multiple outcomes aligned, where one outcome has a rubric result for a student" do @@ -103,7 +94,6 @@ describe OutcomeResultResolverHelper do create_outcome create_alignment lor = create_learning_outcome_result @students[0], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] # We cannot have two LORs for the same student, assignment, and outcome in the db lor.workflow_state = "deleted" @@ -112,7 +102,7 @@ describe OutcomeResultResolverHelper do create_alignment_with_rubric({ assignment: @assignment }) create_learning_outcome_result_from_rubric @students[0], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end end @@ -121,46 +111,42 @@ describe OutcomeResultResolverHelper do create_outcome create_alignment create_learning_outcome_result @students[0], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end it "if there is no rubric result for that student" do create_outcome create_alignment create_learning_outcome_result @students[0], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] create_alignment_with_rubric({ assignment: @assignment }) create_learning_outcome_result_from_rubric @students[1], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end it "if there is no rubric result for that assignment" do create_outcome create_alignment create_learning_outcome_result @students[2], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] create_alignment_with_rubric create_learning_outcome_result_from_rubric @students[2], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end it "if there is no rubric result for that outcome" do create_outcome create_alignment create_learning_outcome_result @students[0], 1.0 - authoritative_results = JSON.parse(authoritative_results_from_db)["results"] create_outcome create_alignment_with_rubric create_learning_outcome_result_from_rubric @students[0], 1.0 - expect(resolve_outcome_results(authoritative_results).size).to eq 1 + expect(resolve_outcome_results(authoritative_results_from_db).size).to eq 1 end end end diff --git a/spec/helpers/outcomes_service_authoritative_results_helper_spec.rb b/spec/helpers/outcomes_service_authoritative_results_helper_spec.rb index 4ff786d081e..47b8e3e595c 100644 --- a/spec/helpers/outcomes_service_authoritative_results_helper_spec.rb +++ b/spec/helpers/outcomes_service_authoritative_results_helper_spec.rb @@ -133,24 +133,21 @@ describe OutcomesServiceAuthoritativeResultsHelper do # Mocks calls to the OS endpoints: # # - retrieving data from the Canvas' LearningOutcomeResult table - # - transforming this data into a collection of JSON AuthoritativeResult objects + # - transforming this data into a collection of AuthoritativeResult objects def authoritative_results_from_db - { - results: - LearningOutcomeResult.all.map do |lor| - { - user_uuid: lor.user.uuid, - points: lor.score, - points_possible: lor.possible, - external_outcome_id: lor.learning_outcome.id, - attempts: nil, - associated_asset_type: nil, - associated_asset_id: lor.alignment.content_id, - artifact: lor.artifact, - submitted_at: lor.submitted_at - } - end - }.to_json + LearningOutcomeResult.all.map do |lor| + { + user_uuid: lor.user.uuid, + points: lor.score, + points_possible: lor.possible, + external_outcome_id: lor.learning_outcome.id, + attempts: nil, + associated_asset_type: nil, + associated_asset_id: lor.alignment.content_id, + artifact: lor.artifact, + submitted_at: lor.submitted_at + } + end end describe "percentage and mastery calculation" do @@ -163,7 +160,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_learning_outcome_result @students[0], points, { points_possible: 19.0 } end - results = json_to_outcome_results(authoritative_results_from_db) + results = convert_to_learning_outcome_results(authoritative_results_from_db) expect(results.size).to eq 20 results.each do |r| @@ -175,7 +172,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end end - describe "#json_to_outcome_result" do + describe "#convert_to_learning_outcome_result" do it "sets artifact to submission" do create_outcome create_alignment @@ -188,7 +185,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do user_uuid: @students[0].uuid, associated_asset_type: "canvas.assignment.quizzes" } - learning_outcome_result = json_to_outcome_result(ar_hash) + learning_outcome_result = convert_to_learning_outcome_result(ar_hash) expect(learning_outcome_result.artifact_id).to eq submission.id expect(learning_outcome_result.artifact_type).to eq "Submission" @@ -206,7 +203,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_learning_outcome_result @students[0], 3.0 from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.size).to eq 2 from_lor.each_with_index do |ru, i| @@ -233,7 +230,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.size).to eq 0 @@ -251,7 +248,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.size).to eq 1 expect(from_lor[0].count).to eq 2 @@ -270,7 +267,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor[0].score).to eq 3.0 @@ -295,7 +292,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_from_scores [1.0, 2.0, 3.0, 4.0], 1 from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.size).to eq 6 # without sorting the arrays this spec may fail at Flakey Spec Catcher @@ -313,7 +310,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.map(&:score)).to eq [3.75] @@ -332,7 +329,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do end from_lor = rollup_user_results LearningOutcomeResult.all.to_a - from_ar = json_to_rollup_scores(authoritative_results_from_db) + from_ar = rollup_scores(authoritative_results_from_db) expect(from_lor.size).to eq 2 # without sorting the arrays this spec may fail at Flakey Spec Catcher @@ -355,7 +352,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_outcome create_alignment create_learning_outcome_result @students[2], 2.0 - results = json_to_outcome_results(authoritative_results_from_db) + results = convert_to_learning_outcome_results(authoritative_results_from_db) rollups = outcome_results_rollups(results: results, users: @students) os_rollups = outcome_service_results_rollups(results) @@ -379,7 +376,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_outcome create_alignment create_learning_outcome_result @students[1], 3.0 - results = json_to_outcome_results(authoritative_results_from_db) + results = convert_to_learning_outcome_results(authoritative_results_from_db) rollups = outcome_service_results_rollups(results) expect(rollups.count).to eq 2 @@ -393,7 +390,7 @@ describe OutcomesServiceAuthoritativeResultsHelper do create_outcome create_alignment create_learning_outcome_result @students[0], 3.0 - results = json_to_outcome_results(authoritative_results_from_db) + results = convert_to_learning_outcome_results(authoritative_results_from_db) rollups = outcome_service_results_rollups(results) expect(rollups.count).to eq 1 diff --git a/spec/lib/outcomes/result_analytics_spec.rb b/spec/lib/outcomes/result_analytics_spec.rb index a572b39699d..e28cbe05cd0 100644 --- a/spec/lib/outcomes/result_analytics_spec.rb +++ b/spec/lib/outcomes/result_analytics_spec.rb @@ -78,8 +78,14 @@ describe Outcomes::ResultAnalytics do outcome_ids = @outcomes.pluck(:id).join(",") uuids = "#{@students[0].uuid},#{@students[1].uuid},#{@students[2].uuid}" expect(ra).to receive(:get_lmgb_results).with(@course, quiz.id.to_s, "canvas.assignment.quizzes", outcome_ids, uuids).and_return(nil) + opts = { context: @course, users: @students, outcomes: @outcomes } + ra.send(:find_outcomes_service_outcome_results, @teacher, opts) + end - ra.send(:find_outcomes_service_outcome_results, { context: @course, users: @students, outcomes: @outcomes }) + it "returns nil if session user is a student and there are more than 1 users sent in the opts" do + opts = { context: @course, users: @students, outcomes: @outcomes } + results = ra.find_outcomes_service_outcome_results(@student, opts) + expect(results).to eq nil end describe "#handle_outcome_service_results" do diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index 7a553025a3b..192dbbcdbb0 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -5686,6 +5686,48 @@ describe Assignment do end end + describe "scope: exclude_muted_associations_for_user" do + before do + @assignment = assignment_model(course: @course) + end + + context "includes assignment" do + it "posted submission" do + @assignment.submission_for_student(@student).update!(posted_at: Time.zone.now) + expect(Assignment.exclude_muted_associations_for_user(@student).count).to eq 1 + end + + it "unposted submissions with default posting policy" do + # By default, an automatic post policy (post_manually: false) is associated to + # an assignment. Now that post policy is included in exclude_muted_associations + # the outcome result will appear in LMGB/SLMGB. It will not appear for manual + # post policy assignment until the submission is posted. See "manual posting + # policy" test cases below. + expect(Assignment.exclude_muted_associations_for_user(@student).count).to eq 1 + end + + it "not graded assignment with unposted submissions with default posting policy" do + @assignment.update!(grading_type: "not_graded") + expect(Assignment.exclude_muted_associations_for_user(@student).count).to eq 1 + end + + it "not graded assignment with unposted submissions with manual posting policy" do + @assignment.post_policy.update!(post_manually: true) + @assignment.update!(grading_type: "not_graded") + expect(Assignment.exclude_muted_associations_for_user(@student).count).to eq 1 + end + end + + context "excludes assignment" do + it "graded assignment with unposted submissions with manual posting policy" do + submission = Submission.find_by(user_id: @user.id, assignment_id: @assignment.id) + expect(submission.posted?).to eq false + @assignment.post_policy.update!(post_manually: true) + expect(Assignment.exclude_muted_associations_for_user(@student).count).to eq 0 + end + end + end + describe "linked submissions" do shared_examples_for "submittable" do before :once do