Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validation that courses in family share course type #44062

Merged
merged 27 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
91c44f5
trying to add validation to make sure family all has same course type
dmcavoy Dec 15, 2021
cf3a484
Add back in line at end of file
dmcavoy Dec 16, 2021
5a112b6
Merge branch 'staging' into family-name-share-course-type
dmcavoy Dec 16, 2021
ecd60be
Return if the family name is not set
dmcavoy Dec 16, 2021
44428ab
Get validation working for all things in family have same course type
dmcavoy Dec 16, 2021
46234df
Updates to test for validation
dmcavoy Dec 16, 2021
94f28cd
Us the cache for scripts so we can cut down on queries
dmcavoy Dec 16, 2021
a726bf7
Update query count tests
dmcavoy Dec 16, 2021
e7afd0f
Merge branch 'staging' into family-name-share-course-type
dmcavoy Dec 17, 2021
848265a
Refactor validation of course type for family name
dmcavoy Dec 17, 2021
47a8e13
Add helper methods for validation
dmcavoy Dec 17, 2021
4ea3146
add tests for course type validation for family
dmcavoy Dec 17, 2021
fe205bd
Test the case of one course in the family
dmcavoy Dec 17, 2021
3dd4581
Fix a couple tests
dmcavoy Dec 17, 2021
17627d6
Merge branch 'staging' into family-name-share-course-type
dmcavoy Jan 4, 2022
9d7018c
Remove serialized at from script seed test script_json
dmcavoy Jan 5, 2022
f63dd8e
move validate into the objects instead of in the concern
dmcavoy Jan 5, 2022
b6f6b3f
move back to using all instead of all_courses because of query count …
dmcavoy Jan 6, 2022
e45285c
Merge branch 'staging' into family-name-share-course-type
dmcavoy Jan 6, 2022
9e95f85
Update to set up the cache for some tests
dmcavoy Jan 6, 2022
0145989
Pull out populating cache into test helper
dmcavoy Jan 6, 2022
df7ae28
Trying to get tests to pass
dmcavoy Jan 7, 2022
46fbb9c
Remove populate_cache_and disconnect_db from unit_group_test.rb
dmcavoy Jan 8, 2022
80ee695
Just go back to using all instead of all_courses because of tests
dmcavoy Jan 8, 2022
b6319a7
Merge branch 'staging' into family-name-share-course-type
dmcavoy Jan 8, 2022
2412c1d
Improve the error messages for course type validation
dmcavoy Jan 10, 2022
626dd32
Improve error message lines
dmcavoy Jan 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 35 additions & 1 deletion dashboard/app/models/concerns/curriculum/course_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,45 @@ module Curriculum::CourseTypes
validates :participant_audience, acceptance: {accept: SharedCourseConstants::PARTICIPANT_AUDIENCE.to_h.values, message: 'must be facilitator, teacher, or student'}

validate :cannot_have_same_audiences
validate :must_have_same_course_type_as_family
end

# All courses in the same family name must have the save instruction_type, instructor_audience, and participant audience
def must_have_same_course_type_as_family
all_family_courses = get_family_courses
return if all_family_courses.nil_or_empty?

return unless all_family_courses.length > 1
errors.add(:instructor_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:instructor_audience).uniq.length > 1 || all_family_courses.last&.instructor_audience != instructor_audience
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want to keep these all on one line, how about something a bit shorter, like:

Suggested change
errors.add(:instructor_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:instructor_audience).uniq.length > 1 || all_family_courses.last&.instructor_audience != instructor_audience
errors.add(:instructor_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:instructor_audience).uniq != [instructor_audience]

or

Suggested change
errors.add(:instructor_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:instructor_audience).uniq.length > 1 || all_family_courses.last&.instructor_audience != instructor_audience
errors.add(:instructor_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:instructor_audience).any? {|audience| audience != instructor_audience}

errors.add(:participant_audience, 'must be the same for all courses in a family.') if all_family_courses.map(&:participant_audience).uniq.length > 1 || all_family_courses.last&.participant_audience != participant_audience
errors.add(:instruction_type, 'must be the same for all courses in a family.') if all_family_courses.map(&:instruction_type).uniq.length > 1 || all_family_courses.last&.instruction_type != instruction_type
end

# Get the family name for the course based on if its set on the UnitGroup or Unit
def get_course_family_name
is_a?(Script) && unit_group ? unit_group.family_name : family_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per your top-level comment, you could do this more generally as follows:

Suggested change
is_a?(Script) && unit_group ? unit_group.family_name : family_name
get_course_version&.family_name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies @dmcavoy but I'm going to backtrack and suggest that you punt on this for now -- this will make more activerecord calls, which could turn up as extra queries... and you've already had to deal with enough of that in this PR. practically speaking this is just as good, assuming that we are moving all Scripts onto UnitGroups some time soon.

end

# If course we are check is a unit_group or a unit that is in a unit_group check the family_name on the UnitGroup.
# If the course is a unit that is not in a unit_group check the unit for the family_name
def get_family_courses
family_name = get_course_family_name
return nil if family_name.nil_or_empty?

all_family_courses = nil

if is_a?(UnitGroup) || (is_a?(Script) && unit_group)
all_family_courses = UnitGroup.all.select {|c| c.family_name == family_name}
dmcavoy marked this conversation as resolved.
Show resolved Hide resolved
elsif is_a?(Script)
all_family_courses = Script.get_family_from_cache(family_name)
Comment on lines +45 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this section looks hard to move onto get_course_version -- I wouldn't worry about this for now, and we can tackle it when we move all Scripts onto UnitGroup.

end
dmcavoy marked this conversation as resolved.
Show resolved Hide resolved

all_family_courses
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the additions to this file look much easier to follow now, thanks for updating!

end

# Instructor and Participant Audience can not be equal unless they are nil
def cannot_have_same_audiences
errors.add(:instructor_audience, 'You cannot have the same instructor and participant audiences.') if !instructor_audience.nil? && instructor_audience == participant_audience
errors.add(:instructor_audience, 'should be different from participant audiences.') if !instructor_audience.nil? && instructor_audience == participant_audience
end

# Checks if a user can be the instructor for the course. universal instructors and levelbuilders
Expand Down
2 changes: 1 addition & 1 deletion dashboard/test/controllers/scripts_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ class ScriptsControllerTest < ActionController::TestCase
is_course: 'on',
peer_reviews_to_complete: 1,
curriculum_path: 'fake_curriculum_path',
family_name: 'coursea',
family_name: 'fake-family-z',
version_year: '2020',
pilot_experiment: 'fake-pilot-experiment',
editor_experiment: 'fake-editor-experiment',
Expand Down
1 change: 1 addition & 0 deletions dashboard/test/factories/factories.rb
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,7 @@
sequence(:name) {|n| "bogus-script-#{n}"}
published_state "beta"
is_migrated true
instruction_type "teacher_led"
participant_audience "student"
instructor_audience "teacher"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"new_name": null,
"family_name": "test-serialize-seeding-json-family",
"published_state": "beta",
"instruction_type": null,
"instruction_type": "teacher_led",
"instructor_audience": "teacher",
"participant_audience": "student",
"seeding_key": {
Expand Down Expand Up @@ -1056,4 +1056,4 @@
}
}
]
}
}
3 changes: 2 additions & 1 deletion dashboard/test/lib/services/script_seed_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ class ScriptSeedTest < ActiveSupport::TestCase
# 1 query to get all the programming environments
# 1 query to get all the standards frameworks
# 1 query to check for a CourseOffering. (Would be a few more if is_course was true)
# 1 query to check if units in family have the same course type settings
# LevelsScriptLevels has queries which scale linearly with the number of rows.
# As far as I know, to get rid of those queries per row, we'd need to load all Levels into memory. I think
# this is slower for most individual Scripts, but there could be a savings when seeding multiple Scripts.
# For now, leaving this as a potential future optimization, since it seems to be reasonably fast as is.
# The game queries can probably be avoided with a little work, though they only apply for Blockly levels.
assert_queries(86) do
assert_queries(87) do
ScriptSeed.seed_from_json(json)
end

Expand Down
84 changes: 79 additions & 5 deletions dashboard/test/models/concerns/curriculum/course_types_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,43 @@ class CourseTypesTests < ActiveSupport::TestCase
@plc_reviewer = create :plc_reviewer
@levelbuilder = create :levelbuilder

@unit_group = create(:unit_group, name: 'course-instructed-by-teacher', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.teacher, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.student)
# Unit Groups with Units
@unit_group = create(:unit_group, name: 'course-instructed-by-teacher', family_name: 'teacher-unit-groups')
@unit_in_course = create(:script, name: 'unit-in-teacher-instructed-course')
create(:unit_group_unit, script: @unit_in_course, unit_group: @unit_group, position: 1)
@unit_in_course.reload

@course_teacher_to_students = create(:unit_group, name: 'course-teacher-to-student', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.teacher, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.student)
@unit_group_2 = create(:unit_group, name: 'course-instructed-by-teacher-2', family_name: 'teacher-unit-groups')
@unit_in_course_2 = create(:script, name: 'unit-in-teacher-instructed-course-2')
create(:unit_group_unit, script: @unit_in_course_2, unit_group: @unit_group_2, position: 1)
@unit_in_course_2.reload

# UnitGroups without Units
@course_teacher_to_students = create(:unit_group, name: 'course-teacher-to-student', family_name: 'teacher-unit-groups')
@course_facilitator_to_teacher = create(:unit_group, name: 'course-facilitator-to-teacher', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.facilitator, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
@course_universal_instructor_to_teacher = create(:unit_group, name: 'course-universal-instructor-to-teacher', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.universal_instructor, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
@course_plc_reviewer_to_facilitator = create(:unit_group, name: 'course-plc-reviewer-to-facilitator', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.plc_reviewer, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.facilitator)

@unit_teacher_to_students = create(:script, name: 'unit-teacher-to-student', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.teacher, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.student)
# Units not in UnitGroup
@unit_teacher_to_students = create(:script, name: 'unit-teacher-to-student', family_name: 'teacher-units')
@unit_facilitator_to_teacher = create(:script, name: 'unit-facilitator-to-teacher', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.facilitator, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
@unit_universal_instructor_to_teacher = create(:script, name: 'universal-instructor-to-teacher', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.universal_instructor, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
@unit_plc_reviewer_to_facilitator = create(:script, name: 'plc-reviewer-to-facilitator', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.plc_reviewer, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.facilitator)

@unit_teacher_to_students_2 = create(:script, name: 'unit-teacher-to-student-2', family_name: 'teacher-units')
end

test 'create unit_group with same audiences raises error' do
e = assert_raises do
create(:unit_group, name: 'same-audiences', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.teacher, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
end
assert_equal "Validation failed: Instructor audience You cannot have the same instructor and participant audiences.", e.message
assert_equal "Validation failed: Instructor audience should be different from participant audiences.", e.message
end
test 'create script with same audiences raises error' do
e = assert_raises do
create(:script, name: 'same-audiences', instructor_audience: SharedCourseConstants::INSTRUCTOR_AUDIENCE.teacher, participant_audience: SharedCourseConstants::PARTICIPANT_AUDIENCE.teacher)
end
assert_equal "Validation failed: Instructor audience You cannot have the same instructor and participant audiences.", e.message
assert_equal "Validation failed: Instructor audience should be different from participant audiences.", e.message
end

test 'unit in course should check course for if it is a pl course' do
Expand Down Expand Up @@ -215,4 +225,68 @@ class CourseTypesTests < ActiveSupport::TestCase
refute @course_universal_instructor_to_teacher.can_be_participant?(nil)
refute @course_plc_reviewer_to_facilitator.can_be_participant?(nil)
end

test 'get_course_family_name should get family name from UnitGroup if called for Unit in UnitGroup' do
assert_equal @unit_in_course_2.get_course_family_name, 'teacher-unit-groups'
end

test 'get_course_family_name should get family name from Unit if called for Unit that is not in UnitGroup' do
assert_equal @unit_group_2.get_course_family_name, 'teacher-unit-groups'
end

test 'get_course_family_name should get family name from UnitGroup if called for UnitGroup' do
assert_equal @unit_teacher_to_students_2.get_course_family_name, 'teacher-units'
end

test 'get_family_courses should get all UnitGroups with the same family name if called for Unit in UnitGroup' do
assert_equal @unit_in_course_2.get_family_courses.map(&:name), ['course-instructed-by-teacher', 'course-instructed-by-teacher-2', 'course-teacher-to-student']
end

test 'get_family_courses should get all UnitGroups with the same family name if called for UnitGroup' do
assert_equal @unit_group_2.get_family_courses.map(&:name), ['course-instructed-by-teacher', 'course-instructed-by-teacher-2', 'course-teacher-to-student']
end

test 'get_family_courses should get all Unit with the same family name if called for Unit not in UnitGroup' do
assert_equal @unit_teacher_to_students_2.get_family_courses.map(&:name), ['unit-teacher-to-student', 'unit-teacher-to-student-2']
end

test 'get_family_courses should return nil if there is no family name' do
unit_without_family_name = create :script, name: 'no-family-name'
assert_equal unit_without_family_name.get_family_courses, nil
end

test 'should raise error if instruction type does not match rest of course family' do
error = assert_raises do
@unit_group_2.instruction_type = SharedCourseConstants::INSTRUCTION_TYPE.self_paced
@unit_group_2.save!
end

assert_includes error.message, 'Instruction type must be the same for all courses in a family.'
end

test 'should raise error if instructor audience does not match rest of course family' do
error = assert_raises do
@unit_group_2.instructor_audience = SharedCourseConstants::INSTRUCTOR_AUDIENCE.facilitator
@unit_group_2.save!
end

assert_includes error.message, 'Instructor audience must be the same for all courses in a family.'
end

test 'should raise error if participant audience does not match rest of course family' do
error = assert_raises do
@unit_group_2.participant_audience = SharedCourseConstants::PARTICIPANT_AUDIENCE.facilitator
@unit_group_2.save!
end

assert_includes error.message, 'Participant audience must be the same for all courses in a family.'
end

test 'should not raise error when changing course type values for a course that is the only one in its family' do
solo_unit_in_family_name = create :script, name: 'solo-family-name', family_name: 'solo-family-name'
assert_nothing_raised do
solo_unit_in_family_name.participant_audience = SharedCourseConstants::PARTICIPANT_AUDIENCE.facilitator
solo_unit_in_family_name.save!
end
end
end