Move anonymous ID routines to own module

refs GRADE-1057

Test plan:
  - Specs pass
  - In a course with at least one student, create a new assignment
    and make sure there are no errors

Change-Id: If429480ea516177bfed91509d149275190b44931
Reviewed-on: https://gerrit.instructure.com/150069
Reviewed-by: Derek Bender <djbender@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
Tested-by: Jenkins
QA-Review: Spencer Olson <solson@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Adrian Packel 2018-05-11 15:48:25 -05:00
parent e5212f329a
commit 25870e5117
5 changed files with 125 additions and 100 deletions

View File

@ -17,6 +17,7 @@
#
require 'atom'
require 'anonymity'
class Submission < ActiveRecord::Base
include Canvas::GradeValidations
@ -227,29 +228,6 @@ class Submission < ActiveRecord::Base
anonymized.for_assignment(assignment).pluck(:anonymous_id)
end
# Returns a unique short id to be used for anonymous_id. If the
# generated short id is already in use, loop until an available
# one is generated. `anonymous_ids` are unique per assignment.
# This method will throw a unique constraint error from the
# database if it has used all unique ids.
# An optional argument of existing_anonymous_ids can be supplied
# to customize the handling of existing anonymous_ids. E.g. bulk
# generation of anonymous ids where you wouldn't want to
# continuously query the database
def self.generate_unique_anonymous_id(assignment:, existing_anonymous_ids: anonymous_ids_for(assignment))
loop do
short_id = Submission.generate_short_id
break short_id unless existing_anonymous_ids.include?(short_id)
end
end
# base58 to avoid literal problems with prefixed 0 (i.e. when 0x123
# is interpreted as a hex value `0x123 == 291`), and similar looking
# characters: 0/O, I/l
def self.generate_short_id
SecureRandom.base58(5)
end
# see #needs_grading?
# When changing these conditions, update index_submissions_needs_grading to
# maintain performance.
@ -2459,6 +2437,6 @@ class Submission < ActiveRecord::Base
private
def set_anonymous_id
self.anonymous_id = Submission.generate_unique_anonymous_id(assignment: anonymous_id)
self.anonymous_id = Anonymity.generate_id(existing_ids: Submission.anonymous_ids_for(assignment))
end
end

50
lib/anonymity.rb Normal file
View File

@ -0,0 +1,50 @@
#
# Copyright (C) 2018 - present 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/>.
# This file contains routines for creating IDs used for graders and submission
# in place of the objects' actual IDs for Anonymous Moderated Marking.
# The IDs are 5 characters long and in base58 format. These IDs are meant to be
# unique across the graders or submissions for a given assignment: a grader
# for an assignment, for example, can be referenced by their "anonymous" ID
# for that specific assignment so that their default ID is concealed.
module Anonymity
# Returns a unique short id to be used as an anonymous ID. If the
# generated short id is already in use, loop until an available
# one is generated.
# This method will throw a unique constraint error from the
# database if it has used all unique ids.
# An optional argument of existing_ids can be supplied
# to customize the handling of existing ids. E.g. bulk
# generation of anonymous ids where you wouldn't want to
# continuously query the database
def self.generate_id(existing_ids: [])
loop do
short_id = self.generate_short_id
break short_id unless existing_ids.include?(short_id)
end
end
private_class_method
# base58 to avoid literal problems with prefixed 0 (i.e. when 0x123
# is interpreted as a hex value `0x123 == 291`), and similar looking
# characters: 0/O, I/l
def self.generate_short_id
SecureRandom.base58(5)
end
end

View File

@ -15,6 +15,8 @@
# 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 'anonymity'
class DueDateCacher
INFER_SUBMISSION_WORKFLOW_STATE_SQL = <<~SQL_FRAGMENT
CASE
@ -109,10 +111,7 @@ class DueDateCacher
due_date = submission_info[:due_at] ? "'#{submission_info[:due_at].iso8601}'::timestamptz" : 'NULL'
grading_period_id = submission_info[:grading_period_id] || 'NULL'
anonymous_id = Submission.generate_unique_anonymous_id(
assignment: assignment_id,
existing_anonymous_ids: existing_anonymous_ids
)
anonymous_id = Anonymity.generate_id(existing_ids: existing_anonymous_ids)
existing_anonymous_ids << anonymous_id
sql_ready_anonymous_id = Submission.connection.quote(anonymous_id)
values << [assignment_id, student_id, due_date, grading_period_id, sql_ready_anonymous_id]

View File

@ -0,0 +1,70 @@
#
# Copyright (C) 2018 - present 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_relative '../spec_helper'
describe Anonymity do
describe '.generate_id' do
let(:short_id) { 'aB123' }
context 'given no existing_anonymous_ids' do
subject(:generate_id) do
-> { Anonymity.generate_id }
end
it 'creates an anonymous_id' do
allow(Anonymity).to receive(:generate_short_id).and_return(short_id)
expect(generate_id.call).to eql short_id
end
it 'creates a unique anonymous_id when collisions happen' do
first_anonymous_id = short_id
colliding_anonymous_id = first_anonymous_id
unused_anonymous_id = 'eeeee'
allow(Anonymity).to receive(:generate_short_id).exactly(3).times.and_return(
first_anonymous_id, colliding_anonymous_id, unused_anonymous_id
)
first_returned_id = Anonymity.generate_id
second_returned_id = Anonymity.generate_id(existing_ids: [first_returned_id])
expect(second_returned_id).to eql(unused_anonymous_id)
end
end
context 'given a list of existing_anonymous_ids' do
subject do
Anonymity.generate_id(existing_ids: existing_anonymous_ids_fake)
end
let(:existing_anonymous_ids_fake) { double('Array') }
it 'queries the passed in existing_anonymous_ids' do
allow(Anonymity).to receive(:generate_short_id).and_return(short_id)
expect(existing_anonymous_ids_fake).to receive(:include?).with(short_id).and_return(false)
is_expected.to eql short_id
end
end
end
describe '.generate_short_id' do
it 'generates a short id' do
expect(SecureRandom).to receive(:base58).with(5)
Anonymity.generate_short_id
end
end
end

View File

@ -63,78 +63,6 @@ describe Submission do
end
end
describe '.generate_unique_anonymous_id' do
let(:assignment) { @assignment }
let(:short_id) { 'aB123' }
context 'given no existing_anonymous_ids' do
subject(:generate_unique_anonymous_id) do
-> { Submission.generate_unique_anonymous_id(assignment: assignment) }
end
it 'creates an anonymous_id' do
allow(Submission).to receive(:generate_short_id).and_return(short_id)
expect(generate_unique_anonymous_id.call).to eql short_id
end
it 'fetches existing_anonymous_ids' do
expect(Submission).to receive(:anonymous_ids_for).with(assignment).and_call_original
generate_unique_anonymous_id.call
end
it 'creates a unique anonymous_id when collisions happen' do
first_student = @student
second_student = student_in_course(course: @course, active_all: true).user
first_submission = submission_model(assignment: assignment, user: first_student)
second_submission = submission_model(assignment: assignment, user: second_student)
Submission.update_all(anonymous_id: nil)
first_anonymous_id = short_id
colliding_anonymous_id = first_anonymous_id
unused_anonymous_id = 'eeeee'
allow(Submission).to receive(:generate_short_id).exactly(3).times.and_return(
first_anonymous_id, colliding_anonymous_id, unused_anonymous_id
)
anonymous_ids = [first_submission, second_submission].map do |submission|
submission.update!(anonymous_id: generate_unique_anonymous_id.call)
submission.anonymous_id
end
expect(anonymous_ids).to contain_exactly(first_anonymous_id, unused_anonymous_id)
end
end
context 'given a list of existing_anonymous_ids' do
subject do
Submission.generate_unique_anonymous_id(
assignment: assignment,
existing_anonymous_ids: existing_anonymous_ids_fake
)
end
let(:existing_anonymous_ids_fake) { double('Array') }
before do
student_in_course(course: @course, active_all: true)
end
it 'queries the passed in existing_anonymous_ids' do
allow(Submission).to receive(:generate_short_id).and_return(short_id)
expect(existing_anonymous_ids_fake).to receive(:include?).with(short_id).and_return(false)
is_expected.to eql short_id
end
end
end
describe '.generate_short_id' do
it 'generates a short id' do
expect(SecureRandom).to receive(:base58).with(5)
Submission.generate_short_id
end
end
describe '.anonymous_ids_for' do
subject { Submission.anonymous_ids_for(@first_assignment) }