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

Seeding for CourseVersions for .script files #35980

Merged
merged 14 commits into from Aug 13, 2020
Merged

Conversation

uponthesun
Copy link

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

  • Change the key format, for example, from csp-2020 to just 2020, once the new Course model is added.
  • Seeding for CourseVersions for .course files
  • Add other properties to the CourseVersion object during seeding as appropriate

Testing story

  • added unit tests

Reviewer Checklist:

  • Tests provide adequate coverage
  • Code is well-commented
  • New features are translatable or updates will not break translations
  • Relevant documentation has been added or updated
  • User impact is well-understood and desirable
  • Pull Request is labeled appropriately
  • Follow-up work items (including potential tech debt) are tracked and linked

@@ -0,0 +1,3 @@
family_name 'xyz'
version_year '1234'
is_course true
Copy link
Contributor

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?

Copy link
Author

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?

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member

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

Copy link
Author

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?

Copy link
Member

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.

Copy link
Author

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
Copy link
Author

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.

Copy link
Contributor

@dmcavoy dmcavoy Jul 24, 2020

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

Copy link
Member

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.

Copy link
Member

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?

Copy link
Author

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.

Copy link
Member

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
Copy link
Author

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
Copy link
Contributor

@dmcavoy dmcavoy Jul 24, 2020

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?

Copy link
Author

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.

Copy link
Author

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

Copy link
Member

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)

By contrast, the docs for for has_many ... dependent: :destroy are more explicit:

# unique index will be on (course_id, key).
key = "#{content_root.family_name}-#{content_root.version_year}"

unless content_root.is_course?
Copy link
Contributor

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?

Copy link
Member

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?

Copy link
Author

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?

Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

@davidsbailey davidsbailey left a 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
Copy link
Member

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
Copy link
Member

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?

Copy link
Author

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
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

Copy link
Author

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?
Copy link
Member

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
Copy link
Member

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)

By contrast, the docs for for has_many ... dependent: :destroy are more explicit:

Copy link
Member

@davidsbailey davidsbailey left a comment

Choose a reason for hiding this comment

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

Great work, @uponthesun !

Comment on lines 1 to 2
version_year 2019
is_course true
Copy link
Member

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
Copy link
Member

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
Copy link
Contributor

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?

@uponthesun uponthesun merged commit 5cf1f2a into staging Aug 13, 2020
@uponthesun uponthesun deleted the course-version-seeding branch August 13, 2020 17:14
@uponthesun
Copy link
Author

@davidsbailey

yeah, it seems like is_course? doesn't need to be in the interface, doesn't need to be called by CourseVersion, and the Script model could call this something more specific like is_single_unit_course or something. It's up to you whether to tackle that now or not. If the call here to is_course? can be safely removed, then it does kind of seem worth doing. I could understand keeping it for now if we're not 100% confident on that, though.

Meanwhile, I would expect this interface documentation comment to at least include the fields (and, if applicable, methods) on content_root which you call from this class. Right now that looks like it should include: name, family_name, version_year. Can you please add those to these comments? They don't need to be long.

I'll clean this up and add more documentation once I add the Course model and seeding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants