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

Collection skills migration #3376

Merged
merged 11 commits into from
Jul 24, 2017
Merged

Collection skills migration #3376

merged 11 commits into from
Jul 24, 2017

Conversation

wxyxinyu
Copy link
Contributor

This supercedes #3252, and introduces skills to both the frontend and backend.

@codecov-io
Copy link

codecov-io commented Apr 29, 2017

Codecov Report

Merging #3376 into develop will increase coverage by 0.28%.
The diff coverage is 96.99%.

Impacted file tree graph

@@             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
Impacted Files Coverage Δ
...d/domain/collection/CollectionNodeObjectFactory.js 91.42% <ø> (ø) ⬆️
...v/head/domain/collection/SkillListObjectFactory.js 100% <100%> (ø) ⬆️
.../head/domain/collection/CollectionUpdateService.js 96.64% <100%> (+0.81%) ⬆️
.../domain/collection/CollectionSkillObjectFactory.js 100% <100%> (ø)
...n_editor/editor_tab/CollectionLinearizerService.js 100% <100%> (ø) ⬆️
...d/domain/collection/CollectionValidationService.js 100% <100%> (ø) ⬆️
.../head/domain/collection/CollectionObjectFactory.js 97.38% <92.72%> (-2.62%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c50b196...3c13331. Read the comment docs.

@wxyxinyu
Copy link
Contributor Author

wxyxinyu commented May 1, 2017

A couple of points about the PR:

  1. Is it ok that IDs are being generated both in the frontend and backend? If not, how can this be made better?
  2. Should there be more validation happening?

Copy link
Member

@BenHenning BenHenning left a 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:

  1. 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?
  2. Can you make sure CollectionValidatorService and CollectionValidatorServiceSpec is appropriately updated with the new validations that both the backend and frontend collection objects need to pass?

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

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

)

def validate(self):
"""Validates various properties of collection skill."""
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

'The question_ids list has duplicate entries: %s' %
self.question_ids)

for question_id in self.question_ids:
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

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

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?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Is this immutable also?

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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

Copy link
Member

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? :)

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor Author

@wxyxinyu wxyxinyu left a 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.

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

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

raise ValueError(
'Skill with name "%s" already exists.' % skill_name)

skill_id = 's' + str(self.skill_id_count)
Copy link
Contributor Author

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

@@ -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)
Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

yes, SkillListObjectFactorys are used for lists of skill ids in prereq/acquired skills

return this._id;
};

// Returns the name of the skill.
Copy link
Contributor Author

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']);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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)

Copy link
Member

@BenHenning BenHenning left a 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?

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

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.

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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

def from_dict(cls, skill_id, skill_dict):
return cls(
skill_id,
copy.deepcopy(skill_dict['name']),
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor Author

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

copy.deepcopy(skill_dict['question_ids'])
)

def validate(self):
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

"""

for index, node in enumerate(collection_contents['nodes']):
collection_contents['nodes'][index]['prerequisite_skill_ids'] = (
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 slightly confused by this. Why are we assigning prerequisite_skills to prerequisite_skill_ids? The IDs aren't skill names, right?

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

oldNextSkillId

Copy link
Contributor Author

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

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) {
  ...
});

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

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?

Copy link
Contributor Author

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];
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 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.

Copy link
Contributor Author

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.

Copy link
Member

@seanlip seanlip left a 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.

@seanlip
Copy link
Member

seanlip commented Jul 22, 2017

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

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?

Copy link
Member

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

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

Copy link
Member

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.

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

Successfully merging this pull request may close these issues.

4 participants