-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Collection skills migration #3376
Conversation
Codecov Report
@@ Coverage Diff @@
## develop #3376 +/- ##
===========================================
+ Coverage 44.78% 45.07% +0.28%
===========================================
Files 259 260 +1
Lines 20001 20113 +112
Branches 3146 3157 +11
===========================================
+ Hits 8958 9066 +108
- Misses 11043 11047 +4
Continue to review full report at Codecov.
|
A couple of points about the PR:
|
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.
Thanks @wxyxinyu! Couple more things:
- I saw you added new validate functionality in the backend (both in Skill and in Collection); can you make sure there are corresponding tests for the new
validate()
calls? - Can you make sure
CollectionValidatorService
andCollectionValidatorServiceSpec
is appropriately updated with the new validations that both the backend and frontend collection objects need to pass?
core/domain/collection_domain.py
Outdated
@@ -199,9 +207,19 @@ def skills(self): | |||
return set(self.prerequisite_skills) | set(self.acquired_skills) | |||
|
|||
def update_prerequisite_skills(self, prerequisite_skills): | |||
"""Updates prerequisite skills of collection node. | |||
|
|||
Note: Please use the add_collection_node_prerequisite_skill |
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 we don't want people using this or update_acquired_skills
, why don't we rmeove them?
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.
the update functions in collection_services still call these functions to update the actual skills, but those check for e.g. existence of skill ids before adding them.
note to self: this docstring is slightly wrong, it's now collections_services.update_collection_node_prerequisite_skills
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.
Discussed during TL meeting: we want validation to be in the lowest common place, in this case in the domain object since there's no requirement for the services function to need to do any additional validation.
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.
done
core/domain/collection_domain.py
Outdated
|
||
Note: Please use the add_collection_node_prerequisite_skill | ||
and delete_collection_node_prerequisite_skill functions to update | ||
skills, as they check for consistency with the skills dict.""" |
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.
Nit (style): multi-line docstring should have the """
ending on its own line. Ditto elsewhere.
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.
ok
core/domain/collection_domain.py
Outdated
) | ||
|
||
def validate(self): | ||
"""Validates various properties of collection skill.""" |
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.
Nit (style): Single line docstrings should have an empty line between them and the code. Ditto elsewhere.
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.
done
core/domain/collection_domain.py
Outdated
'The question_ids list has duplicate entries: %s' % | ||
self.question_ids) | ||
|
||
for question_id in self.question_ids: |
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 recommend putting this validation before the uniqueness validation for question_ids
. Right now, if I constructed a new CollectionSkill
object from a list a dicts, it would fail in the uniqueness check above because the set() constructor would be trying to uniqueify a list of non-hashable types (dicts), which would be a confusing error. It would fail more clearly in this validation since dicts are not strings.
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.
done
core/domain/collection_domain.py
Outdated
class Collection(object): | ||
"""Domain object for an Oppia collection.""" | ||
|
||
def __init__(self, collection_id, title, category, objective, | ||
language_code, tags, schema_version, nodes, version, | ||
language_code, tags, schema_version, nodes, skills, | ||
skill_id_count, version, | ||
created_on=None, last_updated=None): |
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.
Nit: can you move this to the line above so that it wraps nicely?
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.
done
return this._id; | ||
}; | ||
|
||
// Returns the name of the skill. |
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 immutable also?
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.
could you explain what immutable-ness is?
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.
Immutable means that the caller can't change the internal state of the value.
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, we should be able to rename skills (this is to be implemented in a separate PR)
return new CollectionSkill(skillId, collectionSkillBackendObject); | ||
}; | ||
|
||
// TODO(wxy): Ensure this matches the backend dict elements for |
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 a TODO
or a note to developers?
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, there is a similar TODO for nodes. Is that one meant to be a note for a future test, or a note for developers?
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 any case I changed it to a note to developers, since that seems to make more sense
|
||
// Simply applying a change is adequate for updating the skill list. | ||
var collection = CollectionEditorStateService.getCollection(); | ||
CollectionUpdateService.setAcquiredSkills( | ||
collection, '0', ['skill2', 'skill3']); | ||
collection, '0', ['s1', 'skill3']); |
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.
We seem to be mixing skill names and IDs. Is that intentional?
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, they're all supposed to be skill IDs now
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 going to be addressed in a follow-up commit, then? :)
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.
yeah
@@ -205,12 +208,15 @@ oppia.factory('CollectionLinearizerService', [ | |||
return false; | |||
} | |||
|
|||
var linearNodeList = _getCollectionNodesInPlayableOrder(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.
Why pull out & add these new variables?
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's so that the skill associated to the node can be deleted as well (originally only the node is deleted)
@@ -203,6 +223,85 @@ oppia.factory('CollectionObjectFactory', [ | |||
return skillList; | |||
}; | |||
|
|||
// Gets a new ID for a skill. This should be of the same form as in the | |||
// backend. | |||
Collection.prototype.getNewSkillId = function() { |
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.
You may want to mention in the documentation that this method has side effects (the skillIdCount/nextSkillId field is going to increment)
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.
done
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.
Thanks for the review, I added some comments addressing your comments. The rest I intend to fix in a commit coming up soon.
core/domain/collection_domain.py
Outdated
@@ -199,9 +207,19 @@ def skills(self): | |||
return set(self.prerequisite_skills) | set(self.acquired_skills) | |||
|
|||
def update_prerequisite_skills(self, prerequisite_skills): | |||
"""Updates prerequisite skills of collection node. | |||
|
|||
Note: Please use the add_collection_node_prerequisite_skill |
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.
the update functions in collection_services still call these functions to update the actual skills, but those check for e.g. existence of skill ids before adding them.
note to self: this docstring is slightly wrong, it's now collections_services.update_collection_node_prerequisite_skills
core/domain/collection_domain.py
Outdated
raise ValueError( | ||
'Skill with name "%s" already exists.' % skill_name) | ||
|
||
skill_id = 's' + str(self.skill_id_count) |
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's just to make extra clear that the skill id is a string, not an integer
core/domain/collection_services.py
Outdated
@@ -868,7 +947,10 @@ def update_collection( | |||
'Collection is public so expected a commit message but ' | |||
'received none.') | |||
|
|||
collection = get_collection_by_id(collection_id) |
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 clue. might be a merge error?
@@ -192,6 +211,7 @@ oppia.factory('CollectionObjectFactory', [ | |||
// skills from all collections nodes within this domain object. Please note | |||
// this operation returns a new skill list everytime this function is | |||
// called. | |||
// TODO(wxy): migrate this to use the skills dict instead. |
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.
yes, SkillListObjectFactory
s are used for lists of skill ids in prereq/acquired skills
return this._id; | ||
}; | ||
|
||
// Returns the name of the skill. |
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.
could you explain what immutable-ness is?
|
||
// Simply applying a change is adequate for updating the skill list. | ||
var collection = CollectionEditorStateService.getCollection(); | ||
CollectionUpdateService.setAcquiredSkills( | ||
collection, '0', ['skill2', 'skill3']); | ||
collection, '0', ['s1', 'skill3']); |
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, they're all supposed to be skill IDs now
@@ -205,12 +208,15 @@ oppia.factory('CollectionLinearizerService', [ | |||
return false; | |||
} | |||
|
|||
var linearNodeList = _getCollectionNodesInPlayableOrder(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.
it's so that the skill associated to the node can be deleted as well (originally only the node is deleted)
…lls to acquired_skill_ids
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.
Thanks @wxyxinyu, this is looking awesome! It's really coming together now. :D I had a few comments regarding consistency in naming, and making sure we have enough and well-written tests. That being said, I'm also slightly concerned over SkillListObjectFactory and whether we're properly keeping it updated now. Have you tested the collection editor flows manually to make sure everything is still displaying & working correctly?
core/domain/collection_domain.py
Outdated
@@ -36,8 +36,8 @@ | |||
COLLECTION_PROPERTY_OBJECTIVE = 'objective' | |||
COLLECTION_PROPERTY_LANGUAGE_CODE = 'language_code' | |||
COLLECTION_PROPERTY_TAGS = 'tags' | |||
COLLECTION_NODE_PROPERTY_PREREQUISITE_SKILLS = 'prerequisite_skills' | |||
COLLECTION_NODE_PROPERTY_ACQUIRED_SKILLS = 'acquired_skills' | |||
COLLECTION_NODE_PROPERTY_PREREQUISITE_SKILLS = 'prerequisite_skill_ids' |
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 this be renamed to COLLECTION_NODE_PROPERTY_PREREQUISITE_SKILL_IDS
? It's not actually changing skills now. Plus, it's technically a new command.
Can you add a comment to the beginning of the section explaining that 'prerequisite_skills' and 'acquired_skills' are now reserved and shouldn't be reused in the future? There's an example of doing this in exp_domain.
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.
done
@@ -68,7 +68,7 @@ def get(self, collection_id): | |||
'is_logged_in': bool(self.user_id), | |||
'collection_id': collection_id, | |||
'collection_title': collection.title, | |||
'collection_skills': collection.skills, | |||
'collection_skills': collection.skills.keys(), |
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.
Might we instead introduce a property like skill_names
or skill_ids
? Can you also make sure we properly refer to skills, skill IDs, and skill names correctly in all places referring to skills? You're changing the meaning of what 'skill' is and we want to be careful not to use the wrong word, as it will just introduce confusion. In this case, I suspect this ID should actually be collection_skill_ids
.
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 isn't being used anywhere; I removed it
core/domain/collection_domain.py
Outdated
def from_dict(cls, skill_id, skill_dict): | ||
return cls( | ||
skill_id, | ||
copy.deepcopy(skill_dict['name']), |
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 it worth deep copying strings? I thought strings are immutable in Python.
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.
fixed
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.
the deepcopy seems to be done in other locations though
core/domain/collection_domain.py
Outdated
copy.deepcopy(skill_dict['question_ids']) | ||
) | ||
|
||
def validate(self): |
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.
Are there tests verifying each of these new potential validation failures?
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.
done
core/domain/collection_domain.py
Outdated
""" | ||
|
||
for index, node in enumerate(collection_contents['nodes']): | ||
collection_contents['nodes'][index]['prerequisite_skill_ids'] = ( |
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 slightly confused by this. Why are we assigning prerequisite_skills
to prerequisite_skill_ids
? The IDs aren't skill names, right?
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.
yeah, I just did the transformation in a really convoluted way
oppia.constant( | ||
'COLLECTION_NODE_PROPERTY_PREREQUISITE_SKILLS', 'prerequisite_skills'); | ||
oppia.constant('COLLECTION_NODE_PROPERTY_ACQUIRED_SKILLS', 'acquired_skills'); | ||
'COLLECTION_NODE_PROPERTY_PREREQUISITE_SKILLS', 'prerequisite_skill_ids'); |
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 these constants also be renamed, per the similar comment earlier in collection_domain?
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.
done
* undo/redo service. | ||
*/ | ||
addCollectionSkill: function(collection, skillName) { | ||
var oldnextSkillId = collection.getNextSkillId(); |
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.
oldNextSkillId
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.
done
@@ -117,6 +117,17 @@ oppia.factory('CollectionValidationService', [ | |||
'learner.'); | |||
} | |||
|
|||
var collectionSkills = collection.getCollectionSkills(); |
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.
You could also use map()
here, e.g.
collection.getCollectionSkills().map(function(collectionSkill) {
return collectionSkill.getName();
}).forEach(function(skillName, index) {
...
});
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 how this would work, since skills is an object and not an array.
SkillList.prototype.removeSkillByName = function(skillName) { | ||
var index = this.indexOfSkill(skillName); | ||
SkillList.prototype.removeSkillById = function(skillId) { | ||
var index = this.indexOfSkill(skillId); |
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 indexOfSkill
be renamed to indexOfSkillId
?
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 refactoring the SkillList-related naming for now, since it might get removed entirely.
var nodeIndex = findNodeIndex(linearNodeList, explorationId); | ||
var collectionNode = linearNodeList[nodeIndex]; | ||
var acquiredSkill = | ||
collectionNode.getAcquiredSkillList().getSkills()[0]; |
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 a bit iffy about using index 0; is there a better way for us to find out the skill ID associated with the exploration ID? Maybe using the exploration title to look up the corresponding skill ID? I'm not sure whether we can make the guarantee that acquired skills are going to remain ordered, or if we should.
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 the only guarantee that we're using is the linearity of the structure (there's only 1 element in the array), and that each acquired skill is only given by one node (so we should delete the acquired skill). The only way these assumptions would seem to change is if the concept of a "linearlizer" changes.
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.
Hi @wxyxinyu -- @BenHenning had less time than he expected leaving for his trip, and couldn't look at it in detail, but he sent me this note regarding your PR: "I trust your opinion on Xinyu's PR. It looked fine to me after the last big round of comments, other than the one thing I pointed out -- i.e. the skill list object factory bit. Since it sounds like that was resolved, I defer judgement to you."
My opinion is that if you think it's good, I'm happy, and I'm going to give LGTM. (I did do a quick skim and I didn't notice anything obviously wrong.) That said, if there are any specific parts you feel iffy about or would like me to look at prior to merge, could you please flag them for me? I would be happy to give you a second opinion on those.
Finally, thank you very much for doing this! I'm glad we can get the collections stuff in good shape for introducing questions.
Update: @wxyxinyu will do a pass and test everything again, prior to final merge. |
@@ -592,6 +733,48 @@ def _convert_collection_contents_v2_dict_to_v3_dict( | |||
return collection_contents | |||
|
|||
@classmethod | |||
def _convert_collection_contents_v3_dict_to_v4_dict( |
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.
@seanlip can you look at this function?
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.
LGTM
@@ -184,8 +184,11 @@ oppia.factory('CollectionLinearizerService', [ | |||
var linearNodeList = _getCollectionNodesInPlayableOrder(collection); | |||
CollectionUpdateService.addCollectionNode( | |||
collection, explorationId, summaryBackendObject); | |||
var skillName = summaryBackendObject.title; |
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.
@seanlip and also the changes in this file
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.
LGTM assuming we're using skills where we need to use skills, and skill IDs where we need to use skill IDs.
This supercedes #3252, and introduces skills to both the frontend and backend.