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
Seeding for CourseVersions for .script files #35980
Conversation
@@ -0,0 +1,3 @@ | |||
family_name 'xyz' | |||
version_year '1234' | |||
is_course true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any benefit to having some lesson groups, lessons and levels in here if this is meant to be used for an integration test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see it, even though I'm calling it an "integration test" for lack of better terms, I want to test something relatively specific around seeding and creating CourseVersions. I don't think lesson groups / lessons / levels should affect that. Were you thinking of anything in particular?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I didn't have anything specific was just wondering
@@ -144,6 +144,7 @@ def generate_plc_objects | |||
project_sharing | |||
curriculum_umbrella | |||
tts | |||
is_course |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should is_course be a column or a property? It seems like we might want to be able to search for all the scripts that are courses easily? Does content root allow us to do that so we aren't worried about using this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this field seems like redundant state. how about this instead:
def is_course
!!course_version
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I remember why I did this now. This field is only really needed to determine whether we should create a CourseVersion to associate with this Script in the seeding process. (Looking at presence of version_year
doesn't work there because scripts which aren't content roots have those too).
This is the easiest way to get that piece of information from the .script file into the seeding logic. I could explicitly avoid storing it in the DB by leaving it off the properties here, though it'd still have to be included in build_property_hash
(or we'd have to refactor how this works.) Not sure if that's any better.
Which do you prefer? Any other ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points Winter. I think the redundant state is ok as long as it serves a purpose, which it sounds like it does. So either option seems fine, so in that sense it doesn't seem worth redoing. Another idea (since you asked 😉 ) would be to add a validation enforcing that !!course_version == is_course
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this validation might cause problems when we update the Script object from the DSL properties, which happens before we would create the corresponding CourseVersion objects.
I could believe there's a good way to refactor things to make this all better but it's not obvious to me right now. I'm open to continuing to iterate on this, but I'll leave this as-is for this PR.
@@ -18,4 +18,31 @@ | |||
|
|||
class CourseVersion < ApplicationRecord | |||
belongs_to :content_root, polymorphic: true | |||
|
|||
def self.update_course_version(content_root) | |||
# TODO: Once the Course model is added, change this to just be version_year, since then the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this seem right? I was thinking having the family name in the key will be redundant long-term.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why CourseVersion would not be the owner of version year instead of script
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why CourseVersion would not be the owner of version year instead of script
IIRC the way we shortened the timeline for this undertaking was to have these properties continue to live on the content root, and as we add new features, we could look for them on the CourseVersion and the CourseVersion could delegate the call to the content root. it would look like this:
class CourseVersion < ApplicationRecord
delegate :family_name, to: :content_root
And then instance methods (but not this class method), could just call family_name
instead of content_root.family_name
.
In the future, we would move the family_name fully into the CourseVersion, in which case I'm not sure how the code below would need to be updated because the content_root here might not have a Course Version yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# TODO: Once the Course model is added, change this to just be version_year, since then the
# unique index will be on (course_id, key).
Does this seem right? I was thinking having the family name in the key will be redundant long-term.
This makes sense to me. Once we have created the new Course model, won't it make more sense to have version_year be it's own db field and just index directly on (course_id, version_year), though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that option, would we just get rid of key
? I was thinking that logically it'd be the same as what you're saying, we'd just call it key
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed, I think those two solutions would be equivalent, just with the column named either version_year
or key
.
content_root: content_root, | ||
) | ||
|
||
# Delete old CourseVersion if the key has been changed to something else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure the right way to handle this, this was my attempt.
|
||
# Delete old CourseVersion if the key has been changed to something else | ||
content_root.course_version.destroy if course_version != content_root.course_version | ||
content_root.course_version = course_version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you just do this does it not delete the old course version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not, as far as I can tell when I was testing this out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's what I get when I try that. I think what happens is ActiveRecord just tries to set the CourseVersion object's content_root to nil. So it tries to unassociate it rather than delete it. Not sure if there's some setting or some other approach for getting it to delete instead.
[1] pry(CourseVersion)> content_root
=> #<Script:0x00007fc0d62287a8
id: 56,
name: "test-all-properties",
created_at: Tue, 11 Aug 2020 05:02:28 UTC +00:00,
updated_at: Tue, 11 Aug 2020 05:02:28 UTC +00:00,
wrapup_video_id: nil,
hidden: true,
user_id: nil,
login_required: false,
properties: {},
new_name: nil,
family_name: nil>
[2] pry(CourseVersion)> content_root.course_version
=> #<CourseVersion:0x00007fc112679960
id: 7,
key: "-2020",
display_name: "2020",
properties: nil,
content_root_type: "Script",
content_root_id: 56,
created_at: Tue, 11 Aug 2020 05:02:28 UTC +00:00,
updated_at: Tue, 11 Aug 2020 05:02:28 UTC +00:00>
[3] pry(CourseVersion)> content_root.course_version = nil
ActiveRecord::StatementInvalid: Mysql2::Error: Column 'content_root_id' cannot be null: UPDATE `course_versions` SET `content_root_id` = NULL, `updated_at` = '2020-08-11 05:02:48' WHERE `course_versions`.`id` = 7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question Dani, I was curious about this too. It's not explicitly spelled out in the docs, but it's possible that the dependent: :destroy
option is what makes this work the way you're describing in other situations. Here are the parts of the docs which seem relevant (this is a has_one and is also polymorphic)
- https://guides.rubyonrails.org/association_basics.html#options-for-has-one-dependent These docs mention destroying the associated object when the parent is deleted, but does not explicitly say the same will happen upon assignment.
- https://guides.rubyonrails.org/association_basics.html#polymorphic-associations doesn't mention destroying so not very helpful
By contrast, the docs for for has_many ... dependent: :destroy
are more explicit:
-
https://guides.rubyonrails.org/association_basics.html#dependent
The :destroy and :delete_all options also affect the semantics of the collection.delete and collection= methods by causing them to destroy associated objects when they are removed from the collection.
# unique index will be on (course_id, key). | ||
key = "#{content_root.family_name}-#{content_root.version_year}" | ||
|
||
unless content_root.is_course? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this here if someone were to change is_course from true to false? Maybe a comment explaining that would be helpful. Also again wondering does do the content_root.course_version =
delete the old object and if so could we just set that to nil for this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is just too early in the morning (😆 ) but I think I am getting confused about what is_course means. In a script file, it made sense to me as "this is a script which represents an entire course". what would it mean for unit_group.is_course?
to be true?
Separately, I don't see where unit_group.is_course?
would be defined so it looks like this might not be working when the content root is a unit group. Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I haven't added that case yet (listed under future work). I think the implementation there should just be adding an is_course
method to the UnitGroup class which always returns true, since AFAIK every unit group we have right now corresponds to a top level CourseVersion.
Do you think this makes sense? Is there a clearer way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that makes sense, but in that case i think what's missing here is an "interface" defining the methods and contracts that unit_group and script are both required to implement in order to be able to be a content root. I can't remember what we found earlier about whether this is possible to accomplish in ruby, but if it's not possible to truly implement an interface then some comments on the content_root association describing the interface could be an acceptable substitute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added comments to document the "Interface" as it is so far in course_version.rb. I can explore the unit test based "interface" enforcement when I work on adding seeding for .course files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the long delay on reviewing this!
@@ -18,4 +18,31 @@ | |||
|
|||
class CourseVersion < ApplicationRecord | |||
belongs_to :content_root, polymorphic: true | |||
|
|||
def self.update_course_version(content_root) | |||
# TODO: Once the Course model is added, change this to just be version_year, since then the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why CourseVersion would not be the owner of version year instead of script
IIRC the way we shortened the timeline for this undertaking was to have these properties continue to live on the content root, and as we add new features, we could look for them on the CourseVersion and the CourseVersion could delegate the call to the content root. it would look like this:
class CourseVersion < ApplicationRecord
delegate :family_name, to: :content_root
And then instance methods (but not this class method), could just call family_name
instead of content_root.family_name
.
In the future, we would move the family_name fully into the CourseVersion, in which case I'm not sure how the code below would need to be updated because the content_root here might not have a Course Version yet.
content_root.course_version.destroy if course_version != content_root.course_version | ||
content_root.course_version = course_version | ||
|
||
# TODO: add relevant properties from content root to course_version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this mean exactly -- are we talking about the "delegate" syntax from earlier?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the short term, yes, and for the long term, to move the properties to actually be stored on this model.
@@ -144,6 +144,7 @@ def generate_plc_objects | |||
project_sharing | |||
curriculum_umbrella | |||
tts | |||
is_course |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this field seems like redundant state. how about this instead:
def is_course
!!course_version
end
@@ -18,4 +18,31 @@ | |||
|
|||
class CourseVersion < ApplicationRecord | |||
belongs_to :content_root, polymorphic: true | |||
|
|||
def self.update_course_version(content_root) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add some comments on this method saying what it does?
@@ -18,4 +18,31 @@ | |||
|
|||
class CourseVersion < ApplicationRecord | |||
belongs_to :content_root, polymorphic: true | |||
|
|||
def self.update_course_version(content_root) | |||
# TODO: Once the Course model is added, change this to just be version_year, since then the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# TODO: Once the Course model is added, change this to just be version_year, since then the
# unique index will be on (course_id, key).
Does this seem right? I was thinking having the family name in the key will be redundant long-term.
This makes sense to me. Once we have created the new Course model, won't it make more sense to have version_year be it's own db field and just index directly on (course_id, version_year), though?
@@ -144,6 +144,7 @@ def generate_plc_objects | |||
project_sharing | |||
curriculum_umbrella | |||
tts | |||
is_course |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points Winter. I think the redundant state is ok as long as it serves a purpose, which it sounds like it does. So either option seems fine, so in that sense it doesn't seem worth redoing. Another idea (since you asked 😉 ) would be to add a validation enforcing that !!course_version == is_course
.
@@ -919,7 +920,7 @@ def self.setup(custom_files, new_suffix: nil, new_properties: {}) | |||
added_scripts = scripts_to_add.sort_by.with_index {|args, idx| [args[0][:id] || Float::INFINITY, idx]}.map do |options, raw_lesson_groups| | |||
add_script(options, raw_lesson_groups, new_suffix: new_suffix, editor_experiment: new_properties[:editor_experiment]) | |||
rescue => e | |||
raise e, "Error adding script named '#{options[:name]}': #{e}" | |||
raise e, "Error adding script named '#{options[:name]}': #{e}", e.backtrace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for my own information, did this end up being helpful? based on the PR description where I added this, it looks like I was getting the backtraces already: #35660 but I'm not clear based on my output whether some of it just became part of the "message" part of the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I posted my before and after above, not sure why I was getting a different result from you. I can dig into this a bit more later.
# unique index will be on (course_id, key). | ||
key = "#{content_root.family_name}-#{content_root.version_year}" | ||
|
||
unless content_root.is_course? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is just too early in the morning (😆 ) but I think I am getting confused about what is_course means. In a script file, it made sense to me as "this is a script which represents an entire course". what would it mean for unit_group.is_course?
to be true?
Separately, I don't see where unit_group.is_course?
would be defined so it looks like this might not be working when the content root is a unit group. Am I missing something?
|
||
# Delete old CourseVersion if the key has been changed to something else | ||
content_root.course_version.destroy if course_version != content_root.course_version | ||
content_root.course_version = course_version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question Dani, I was curious about this too. It's not explicitly spelled out in the docs, but it's possible that the dependent: :destroy
option is what makes this work the way you're describing in other situations. Here are the parts of the docs which seem relevant (this is a has_one and is also polymorphic)
- https://guides.rubyonrails.org/association_basics.html#options-for-has-one-dependent These docs mention destroying the associated object when the parent is deleted, but does not explicitly say the same will happen upon assignment.
- https://guides.rubyonrails.org/association_basics.html#polymorphic-associations doesn't mention destroying so not very helpful
By contrast, the docs for for has_many ... dependent: :destroy
are more explicit:
-
https://guides.rubyonrails.org/association_basics.html#dependent
The :destroy and :delete_all options also affect the semantics of the collection.delete and collection= methods by causing them to destroy associated objects when they are removed from the collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, @uponthesun !
version_year 2019 | ||
is_course true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert?
assert_nil CourseVersion.find_by(key: 'csz-2050') | ||
end | ||
|
||
test "update_course_version does nothing for script without CourseVersion if is_course is false" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great test cases, especially the descriptions! these really helped me understand the expected behavior.
assert_equal '1234', course_version.display_name | ||
end | ||
|
||
test "update_course_version creates CourseVersion for script that doesn't have one if is_course is true" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like you updated the name of update_course_version method so should it get updated in the name of the tests?
I'll clean this up and add more documentation once I add the Course model and seeding. |
Create, update, and delete CourseVersions during seeding process, based on the presence of
is_course true
in the .script file. Does not handle .course files yet, but it should work similarly for them.Future work
csp-2020
to just2020
, once the new Course model is added.Testing story
Reviewer Checklist: