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

Playthrough Visualisation: Milestone 1.2 (Part 2) #4946

Merged
merged 16 commits into from
May 23, 2018
Merged

Playthrough Visualisation: Milestone 1.2 (Part 2) #4946

merged 16 commits into from
May 23, 2018

Conversation

pranavsid98
Copy link
Contributor

@pranavsid98 pranavsid98 commented May 16, 2018

This PR implements Milestone 1.2 of the Playthrough Visualisation project (can be found at Learner Playthrough Visualization ).

The following features are implemented in this PR:

  • StorePlaythrough controller along with tests.

@seanlip @brianrodri PTAL! This is the most complex controller with a lot of conditionals, so I'll make another commit after adding more tests.

Thanks a lot

@pranavsid98 pranavsid98 requested review from brianrodri and seanlip May 16, 2018 13:50
@codecov-io
Copy link

codecov-io commented May 16, 2018

Codecov Report

Merging #4946 into develop will increase coverage by 0.39%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #4946      +/-   ##
===========================================
+ Coverage    44.16%   44.56%   +0.39%     
===========================================
  Files          386      387       +1     
  Lines        23289    23345      +56     
  Branches      3789     3800      +11     
===========================================
+ Hits         10286    10403     +117     
+ Misses       13003    12942      -61
Impacted Files Coverage Δ
...ev/head/components/CodemirrorMergeviewDirective.js 5.55% <0%> (-0.33%) ⬇️
...v/head/pages/learner_dashboard/LearnerDashboard.js 2.27% <0%> (-0.02%) ⬇️
...main/utilities/FileDownloadRequestObjectFactory.js 100% <0%> (ø) ⬆️
...ev/head/domain/utilities/AudioFileObjectFactory.js 100% <0%> (ø) ⬆️
...ead/domain/utilities/AudioLanguageObjectFactory.js 100% <0%> (ø) ⬆️
...ilities/AutogeneratedAudioLanguageObjectFactory.js 100% <0%> (ø) ⬆️
...ev/head/services/stateful/BackgroundMaskService.js 100% <0%> (ø) ⬆️
...ev/head/domain/utilities/ImageFileObjectFactory.js 100% <0%> (ø)
...ploration_player/AudioTranslationManagerService.js 75.75% <0%> (+0.75%) ⬆️
...ctions/InteractiveMap/directives/InteractiveMap.js 27.84% <0%> (+0.92%) ⬆️
... and 4 more

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 0828d8c...0f19145. Read the comment docs.

Copy link
Contributor

@brianrodri brianrodri left a comment

Choose a reason for hiding this comment

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

Thanks Pranav, I've taken my first pass! I see mostly stylistic issues, the logic looks good to me!

# either 'state_name' or 'state_names'.
state_name_in_customization_args = (
True
if 'state_name' in customization_args else False)
Copy link
Contributor

@brianrodri brianrodri May 17, 2018

Choose a reason for hiding this comment

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

This can be simplified to

state_name_in_customization_args = 'state_name' in customization_args

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh. right. Makes sense. Done.

customization_args['state_name']))) or (
not state_name_in_customization_args and (
issue_customization_args['state_names'] == (
customization_args['state_names']))):
Copy link
Contributor

@brianrodri brianrodri May 17, 2018

Choose a reason for hiding this comment

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

This expression is rather hard to read, here are some ideas for improvement:

  • Move it to a function with many simple return True or return False statements.
  • Split the expression into more variables (like you've done with state_name_in_customization_args)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did the first point.

exp_issues.unresolved_issues.append(issue)

stats_services.save_exp_issues_model_transactional(
exp_issues)
Copy link
Contributor

Choose a reason for hiding this comment

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

This statement should fit on one line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah true. Done.

self.playthrough_data['issue_customization_args']['state_name'][
'value'] = 'state_name2'

self.post_json('/explorehandler/store_playthrough/%s' % (
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: prefer keeping expressions together on a single line, e.g.:

self.post_json(
    '/explorehandler/store_playthrough/%s' % (self.exp_id),
    {
        'playthrough_data': self.playthrough_data,
        'exp_issue': self.exp_issue
    })

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.

"""Test that a new cyclic playthrough gets added to the correct
cyclic issue when it exists.
"""
playthrough_id = stats_models.PlaythroughModel.create(

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

})
model.put()


Copy link
Contributor

Choose a reason for hiding this comment

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

nit: only use 2 blank lines between classes.

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.

customization_args: dict. The customization dict for the issue of
the playthrough to be stored.
"""
if state_name_present:
Copy link
Contributor

@brianrodri brianrodri May 17, 2018

Choose a reason for hiding this comment

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

Since all the data you need to determine this value is already passed in as parameters, it'd make more sense to make it inline:

if 'state_name' in issue_customization_args:
    # use 'state_name'
else:
    # use 'state_names'

Or maybe even

state_name_key = (
    'state_name' if 'state_name' in issue_customization_args else 'state_names')

return issue_customization_args[state_name_key] == customization_args[state_name_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 -- having state_name_present passed in as an arg is icky.

Also this is quite fragile. Better to do it based on type: if issue_type = ... then do a certain check; else if issue_type = ... then do another check, etc. That way it's easy to augment when new issue types are added in the future.

Copy link
Member

Choose a reason for hiding this comment

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

Even better: have a static dict mapping from issue_type to issue_keyname (either 'state_name' or 'state_names'). Then get the relevant keyname from that dict and look it up. Make sure to add comments to explain what an "issue keyname" is and what it's used for.

Also, if you're using state_names as a key, is it sorted? Is [a, b, c] considered the same key as [b, c, a]? That's worth a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@brianrodri @seanlip The last comment seems sensible to implement. I've added a mapping for the same, and its significantly reduced lines of code too. Thanks for pointing it out.

Re the comment for state_names, ordering is important and Ive added a comment to state the same.

if issue['issue_id'] == exp_issue_dict['issue_id']:
issue_customization_args = issue['issue_customization_args']
# This check checks if the state name or the list of state names
# (depending on the issue type) are identical.
Copy link
Contributor

@brianrodri brianrodri May 17, 2018

Choose a reason for hiding this comment

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

This comment doesn't belong here, the function's name does a fine job of explaining what's happening here. Move it to the function's docstring, since it describes the implementation.

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

@brianrodri brianrodri left a comment

Choose a reason for hiding this comment

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

Getting close, thanks!

Also, I still see some lines that should fit on a single line. It really helps readability to have less eye-movement, so try to fit expressions together into a single line when possible!

e.g., prefer:

someFunctionCall(
    somethingElse('with string', 'and int:', 5),
    andThen(
        'some long string that takes up a lot of space',
        # '[32,' could fit on previous line, but should remain "tight" with entire list.
        [32, 55]),
    5)

class StorePlaythroughHandler(base.BaseHandler):
"""Handles a useful playthrough coming in from the frontend to store it."""

REQUIRE_PAYLOAD_CSRF_CHECK = False
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment to explain what this is/what it is used for.

Copy link
Member

Choose a reason for hiding this comment

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

Actually @pranavsid98 this seems incorrect. We should have a CSRF check. Why bypass it?

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.

if self._does_exp_issue_exist_in_exp_issues_model(
state_name_in_customization_args,
issue_customization_args, customization_args):
if len(issue['playthrough_ids']) < (
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: move the parenthesis to wrap the entire expression:

if (len(issue['playthrough_ids']) <
        feconf.MAX_PLAYTHROUGHS_FOR_ISSUE):

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.


self.login(self.VIEWER_EMAIL)
self.signup(self.VIEWER_EMAIL, self.VIEWER_USERNAME)
exp_services.delete_demo(self.exp_id)
Copy link
Contributor

@brianrodri brianrodri May 17, 2018

Choose a reason for hiding this comment

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

Does this need to be run for the very first test, or is it to clean-up just in case a demo already exists? In that case, maybe this belongs in def tearDown?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, its not required. Ive removed it.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, please commit that then!

'value': 'state_name1'
}
}
}], True)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: use keyword argument here so it's clearer "what" is going to be set to True.

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.

'value': 'state_name1'
}
}
}], True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same, please add keyword argument label here.

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.

@@ -420,7 +420,7 @@ def __init__(self, exp_id, unresolved_issues):
represents an issue along with the associated playthroughs.
Each dict will be of the form:
{
issue_type: str. Type of the issue.
issue_id: str. ID of the issue.
Copy link
Member

Choose a reason for hiding this comment

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

As an FYI I think issue_type is better. An ID uniquely identifies an issue.

(We got this wrong with interactions -- we should actually have used interaction_type instead of interaction_id, oh well. But let's not propagate that.)

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.

@pranavsid98
Copy link
Contributor Author

@brianrodri @seanlip This is good to go for another pass. PTAL! Thanks :)

Copy link
Contributor

@brianrodri brianrodri left a comment

Choose a reason for hiding this comment

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

Excellent, looks good to me! Have a few more round of requested changes before we can merge :)


@acl_decorators.can_play_exploration
def post(self, exploration_id):
"""Handles POST requests.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please also document what happens to the POST requests, e.g., "appends to existing list of playthroughs or deletes it if already full." It should give me an idea of what the code will do so it's easy to review when I actually read over the code (both today and in the future).

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.


self.login(self.VIEWER_EMAIL)
self.signup(self.VIEWER_EMAIL, self.VIEWER_USERNAME)
exp_services.delete_demo(self.exp_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

OK, please commit that then!

"""Test that a new playthrough gets created and a new issue is created
for it.
"""
self.exp_issue['customization_args']['state_name'][
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: use parenthesis here since the indexing fits on one line:

        self.exp_issue['customization_args']['state_name']['value'] = (
            'state_name2')

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.

ExplorationIssues domain object.

Args:
exp_issues. ExplorationIssues. The exploration issues domain
Copy link
Contributor

Choose a reason for hiding this comment

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

should use a ":" here,

        exp_issues: ExplorationIssues.

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.

ExplorationIssues domain object in a transaction.

Args:
exp_issues. ExplorationIssues. The exploration issues domain
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

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

@brianrodri brianrodri left a comment

Choose a reason for hiding this comment

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

Whoops, forgot to hit Request changes

@seanlip
Copy link
Member

seanlip commented May 18, 2018

@pranavsid98 FYI I'll take one more pass later this evening.

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.

Sorry for the delay, @pranavsid98. Took a careful pass, PTAL.

class StorePlaythroughHandler(base.BaseHandler):
"""Handles a useful playthrough coming in from the frontend to store it."""

@acl_decorators.can_play_exploration
Copy link
Member

Choose a reason for hiding this comment

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

Why "play"? Shouldn't it at least be edit access?

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 Nope. A playthrough of a learner is what's being stored right? The learner user doesnt need to have edit access right?

Copy link
Member

Choose a reason for hiding this comment

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

Why do you want to allow learners to look at other learners' playthroughs?

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 Im a bit confused here. What does this decorator mean? Doesnt it mean that the user that is sending the request should have that particular permission? So, the StorePlaythroughController is sent from the front end during a learner session and technically from the learner user. Why would he need edit access?
Please correct me if Im understanding this wrong here.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, sorry! It's me who understood it wrongly. You're correct. Thank you for pushing back.

else:
# In this case, the playthrough instance created doesn't
# need to be stored, so it is deleted.
stats_models.PlaythroughModel.get(
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it makes sense to speculatively create then delete (two datastore calls). Can we check whether we want to create it first, and then, if so, only create it 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.

True. I modified it to do create only when required instead.


issue_found = False
for index, issue in enumerate(exp_issues.unresolved_issues):
if issue['issue_type'] == exp_issue_dict['issue_type']:
Copy link
Member

Choose a reason for hiding this comment

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

Is there any guarantee that each issue in the list of unresolved issues has a different (type, key) tuple? If so, where is this enforced?

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 Whenever any issue is added to this list, it is done only if the combination of (type, key) doesnt already exist. Isn't that sufficient enough to know there are no duplicates?

Copy link
Member

Choose a reason for hiding this comment

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

Yes as long as this is the only mechanism for adding new issues.

'issue_customization_args'],
'playthrough_ids': [playthrough_id]
}
exp_issues.unresolved_issues.append(issue)
Copy link
Member

Choose a reason for hiding this comment

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

Any problem if there's already a resolved issue with the same issue type and customization args? Will it lead to duplicates when the unresolved issue subsequently gets resolved?

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 Our current system of handling issue resolving is to just remove the issue from the list and delete all associated playthroughs. So, if an issue has the same type and customization args and we've resolved an issue before of the same type, the new issue will still get recorded. This behaviour is desired since the issue is cropping up even after the creator resolves an issue.

Copy link
Member

Choose a reason for hiding this comment

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

OK, sounds good, thanks!

"""Test that a new cyclic playthrough gets added to the correct
cyclic issue when it exists.
"""
playthrough_id = stats_models.PlaythroughModel.create(

This comment was marked as off-topic.

@@ -35,7 +35,7 @@ def get_all_issue_ids(cls):
Returns:
list(str). The list of all allowed issue IDs.
"""
return feconf.ALLOWED_ISSUE_IDS
return feconf.ALLOWED_ISSUE_TYPES
Copy link
Member

Choose a reason for hiding this comment

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

docstring says ids, this says types, please standardize.

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.

issue_id = ndb.StringProperty(indexed=True)
# The customization args dict for the given issue_id.
# Type of the issue.
issue_type = ndb.StringProperty(indexed=True)
Copy link
Member

Choose a reason for hiding this comment

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

Probably want a choices parameter too? See e.g. #4943.

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 Shouldn't we be trying to define all our issue/action types in feconf and use it from there? Is this actually required?

Copy link
Member

Choose a reason for hiding this comment

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

In general the more validation the better. So yes.

(In fact it seems to me like you'll want to add required=True in a bunch of places, too.)

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 customization args dict for the given issue_id.
# Type of the issue.
issue_type = ndb.StringProperty(indexed=True)
# The customization args dict for the given issue_type.
issue_customization_args = ndb.JsonProperty(default=None)
# The playthrough actions for this playthrough. This will be a list of dicts
Copy link
Member

Choose a reason for hiding this comment

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

Should be ndb.JsonProperty(repeated=True) to signify list

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.

issue_customization_args: dict. The customization args dict for the
given issue_id.
given issue_type.
playthrough_actions: list(dict). The playthrough actions for this
playthrough. This will be a list of dicts where each dict
represents a single playthrough action. The list is ordered by
Copy link
Member

Choose a reason for hiding this comment

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

two lines below: bool not Bool.

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.

feconf.py Outdated
'ExplorationStart',
'AnswerSubmit',
'ExplorationQuit'
]

# Mapping from issue type to issue keyname in the issue customization dict. This
# particular mapping is to distinguish between issues which have their keyname
Copy link
Member

Choose a reason for hiding this comment

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

This second sentence isn't great because new types of issues may be added in the future.

Instead, say something about how the field can be used to uniquely identify/characterize the issue.

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 Does it seem better now?

class StorePlaythroughHandler(base.BaseHandler):
"""Handles a useful playthrough coming in from the frontend to store it."""

@acl_decorators.can_play_exploration
Copy link
Member

Choose a reason for hiding this comment

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

Oh, sorry! It's me who understood it wrongly. You're correct. Thank you for pushing back.


issue_found = False
for index, issue in enumerate(exp_issues.unresolved_issues):
if issue['issue_type'] == exp_issue_dict['issue_type']:
Copy link
Member

Choose a reason for hiding this comment

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

Yes as long as this is the only mechanism for adding new issues.

'issue_customization_args'],
'playthrough_ids': [playthrough_id]
}
exp_issues.unresolved_issues.append(issue)
Copy link
Member

Choose a reason for hiding this comment

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

OK, sounds good, thanks!

issue_id = ndb.StringProperty(indexed=True)
# The customization args dict for the given issue_id.
# Type of the issue.
issue_type = ndb.StringProperty(indexed=True)
Copy link
Member

Choose a reason for hiding this comment

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

In general the more validation the better. So yes.

(In fact it seems to me like you'll want to add required=True in a bunch of places, too.)

issue_customization_args = ndb.JsonProperty(default=None)
# The playthrough actions for this playthrough. This will be a list of dicts
# where each dict represents a single playthrough action. The list is
# ordered by the time of occurence of the action.
playthrough_actions = ndb.JsonProperty(default=None)
playthrough_actions = ndb.JsonProperty(default=None, repeated=True)
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 if repeated is specified, you don't need default -- the default value will just be []. Having, additionally, a default of None seems confusing.

(Please double-check the relevant App Engine docs on ndb datastore properties before resolving this.)

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.

# In this case, the playthrough instance created doesn't
# need to be stored, so it is deleted.
stats_models.PlaythroughModel.get(
playthrough_id).delete()
issue_found = True
Copy link
Member

Choose a reason for hiding this comment

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

nit: consider moving this statement up to line 331, since it is at that point in the logic that you are certain that the issue has been found. Easier to read and follow the code that way.

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.

@@ -1044,6 +1044,26 @@ def create(
playthrough_instance.put()
return instance_id

@classmethod
def create_from_dict(cls, playthrough_data):
Copy link
Member

Choose a reason for hiding this comment

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

See above warning about implicitly trusting the playthrough data dict.

Also the layering is a bit off. Stuff like to_dict() and from_dict() should go in the domain layer. See how other objects are saved/created; you shouldn't be passing raw dicts to the storage layer and expecting the storage layer to interpret those (unstructured) dicts. Passing individual args is fine, and calling the constructor directly (like we do elsewhere) is also fine.

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.

EXPLORATION_START_ID = 'ExplorationStart'
ANSWER_SUBMIT_ID = 'AnswerSubmit'
EXPLORATION_QUIT_ID = 'ExplorationQuit'
EXPLORATION_START_TYPE = 'ExplorationStart'
Copy link
Member

Choose a reason for hiding this comment

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

ACTION_TYPE_EXPLORATION_START
ACTION_TYPE_ANSWER_SUBMIT
ACTION_TYPE_EXPLORATION_QUIT

Copy link
Member

Choose a reason for hiding this comment

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

Btw for this and the file below, why are these constants being defined in tests? Shouldn't we be using the canonical references for these constants (like feconf or something)?

Unless your explicit purpose is to mock these, but in that case, it should be something like ACTION_TYPE_FAKE_ACTION_FOR_TESTS to make the intention clear.

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.

EARLY_QUIT_ID = 'EarlyQuit'
MULTIPLE_INCORRECT_SUBMISSIONS_ID = 'MultipleIncorrectSubmissions'
CYCLIC_STATE_TRANSITIONS_ID = 'CyclicStateTransitions'
EARLY_QUIT_TYPE = 'EarlyQuit'
Copy link
Member

Choose a reason for hiding this comment

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

ISSUE_TYPE_EARLY_QUIT, etc.

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.

feconf.py Outdated
# IDs of allowed types of issues.
ALLOWED_ISSUE_IDS = [
# Types of allowed issues.
ALLOWED_ISSUE_TYPES = [
'EarlyQuit',
Copy link
Member

Choose a reason for hiding this comment

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

If you need to, you can also make these individual strings into named constants, and then have

ALLOWED_ISSUE_TYPES = [ISSUE_TYPE_EARLY_QUIT, ...]

Also do these constants really need to be in feconf? If they're being referenced by multiple modules or controllers then maybe that makes sense since that means they're in "shared" territory. But if their usage is fairly narrow then maybe they'd be better placed somewhere like stats_models 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.

Done.

feconf.py Outdated

# Mapping from issue type to issue keyname in the issue customization dict. This
# mapping is useful to uniquely identify issues by the combination of their
# issue type and the concerend state(s).
Copy link
Member

Choose a reason for hiding this comment

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

typo: concerned

Also, change last part to: ... of their issue type and other type-specific information (such as the list of states involved).

This gives more wiggle room in the future if we introduce keys for issues that are not related to states.

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.

@seanlip
Copy link
Member

seanlip commented May 21, 2018

@pranavsid98 I don't see any new changes to review. Did you push your commits?

for playthrough_property in playthrough_properties:
if playthrough_property not in playthrough_data:
raise self.InvalidInputException(
'%s not in playthrough data dict.' % (playthrough_property))
Copy link
Member

Choose a reason for hiding this comment

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

no need parens around playthrough_property

playthrough_data['issue_type'],
playthrough_data['issue_customization_args'],
playthrough_data['playthrough_actions'],
playthrough_data['is_valid'])
issue = {
Copy link
Member

Choose a reason for hiding this comment

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

This should be an Issue domain object, right? Not a dict? (Otherwise unresolved_issues should be unresolved_issues_dicts.)

Generally we convert to dicts only when storing as jsonproperty, but when retrieving then we convert everything (+ subdicts) into domain objects and work with domain objects throughout the logic layer and higher.

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. Required a major revamp to change all sub dicts into sub domain objects, so PTAL @seanlip

# The customization args dict for the given issue_type.
issue_customization_args = ndb.JsonProperty(default=None)
issue_customization_args = ndb.JsonProperty(default=None, required=True)
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need a default value at all here? None is never valid, 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.

Done.

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.

Looks good on the whole, only one issue with schema versions I think.

@@ -1361,10 +1360,10 @@ def test_playthrough_gets_added_to_cyclic_issues(self):
'is_valid': True
}

self.exp_issue = {
self.exp_issue_dict = {
'issue_type': 'CyclicStateTransitions',
'schema_version': 1,
Copy link
Member

Choose a reason for hiding this comment

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

Here and below, I thought you were removing the schema_version key from the stuff that gets passed in?

"""
self.issue_type = issue_type
self.schema_version = schema_version
self.customization_args = customization_args
self.schema_version = stats_models.CURRENT_ISSUE_SCHEMA_VERSION
Copy link
Member

Choose a reason for hiding this comment

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

This seems off. Shouldn't schema_version be passed in here? If you're loading an old issue from the database you can't just change its schema version like this, you need to actually do a migration like in the Exploration object.

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.

'schema_version': self.schema_version,
'customization_args': exp_domain.get_full_customization_args(
self.customization_args,
'issue_customization_args': exp_domain.get_full_customization_args(
Copy link
Member

Choose a reason for hiding this comment

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

Technically this has nothing to do with exp_domain. Perhaps move get_full_customization_args to utils.py instead if multiple parts of the code from different places are using it.

OK to do this in a follow-up PR, but please make sure to track it with an issue and that it actually gets done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Filed a followup issue.

Copy link
Member

Choose a reason for hiding this comment

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

(For record-keeping: please refer to the follow-up issue number in your comment.)

Copy link
Member

Choose a reason for hiding this comment

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

Note: field as #4968.

single key, 'value', whose corresponding value is the value of
the customization arg.
"""
self.action_type = action_type
self.schema_version = schema_version
self.customization_args = customization_args
self.schema_version = stats_models.CURRENT_ACTION_SCHEMA_VERSION
Copy link
Member

Choose a reason for hiding this comment

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

ditto

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

Almost there!

@@ -318,6 +318,7 @@ def _require_exp_issue_dict_is_valid(self, exp_issue_dict):

dummy_exp_issue = stats_domain.ExplorationIssue(
exp_issue_dict['issue_type'],
stats_models.CURRENT_ISSUE_SCHEMA_VERSION,
Copy link
Member

Choose a reason for hiding this comment

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

You probably need to pass the schema version in from the frontend and run this through the upgrade process as needed, rather than just assign CURRENT_ISSUE_SCHEMA_VERSION. This is because the server may update "under" the learner, who is using a frontend that sends a dict following the old schema version.

Or, perhaps simpler, you could just discard the data if the schema version sent in the data doesn't match the schema version in the backend.

Either way, I don't think you can just assume that the latest schema version applies.

Ditto below for actions.

Copy link
Contributor Author

@pranavsid98 pranavsid98 May 23, 2018

Choose a reason for hiding this comment

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

@seanlip That may be true. But, even if the server does update while the front end is sending a dict, it doesn't matter right? After we store it, whenever we use get_from_model(), its going to update the dict from the stored version to the later version anyway. Thoughts?

Edit: Wait, my bad. I get it, lets pass the schema version in from the front end. Ignore the comment.

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.

@@ -413,6 +423,7 @@ def post(self, exploration_id):
playthrough_data['is_valid'])
issue = stats_domain.ExplorationIssue(
playthrough_data['issue_type'],
stats_models.CURRENT_ISSUE_SCHEMA_VERSION,
Copy link
Member

Choose a reason for hiding this comment

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

Ditto, don't just assume current schema version

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.

@@ -1362,7 +1366,6 @@ def test_playthrough_gets_added_to_cyclic_issues(self):

self.exp_issue_dict = {
'issue_type': 'CyclicStateTransitions',
'schema_version': 1,
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you've now deleted this here and added it everywhere else ...

Why isn't this getting caught as a problem?

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 For both actions and issues passed from the front end, I was not initially passing the schema version in, but now that im doing that, Ill fix these tests.

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.

@@ -1437,7 +1442,6 @@ def test_cyclic_issues_of_different_order_creates_new_issue(self):

self.exp_issue_dict = {
'issue_type': 'CyclicStateTransitions',
'schema_version': 1,
Copy link
Member

Choose a reason for hiding this comment

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

Ditto. Unless this is meant to be part of a test that checks that incoming dicts with no schema version are discarded (and if that test doesn't already exist, it 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.

Done.

@@ -601,19 +601,21 @@ class ExplorationIssue(object):
"""Domain object representing an exploration issue."""

def __init__(
self, issue_type, issue_customization_args, playthrough_ids):
self, issue_type, schema_version, issue_customization_args,
Copy link
Member

Choose a reason for hiding this comment

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

Odd to separate issue type and issue customization args by schema version. Maybe move schema version to the beginning or the end (since it describes all the other stuff). (Probably end.)

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 Actually, my train of thought was this: The issue type kind of identifies this exploration issue first. Then, we have the schema version. The issue type and schema version together point to a unique customization_args . When I thought about it like that, this seemed to make more sense?

'issue_customization_args': expected_customization_args,
'playthrough_ids': []
})

def test_update_exp_issue_from_model(self):
Copy link
Member

Choose a reason for hiding this comment

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

This is a really well-written test! Very nice.

@@ -31,6 +32,68 @@
VERSION_ALL = 'all'


def _migrate_issue_schema(exp_issue_dict):
"""Holds the responsibility of performing a step-by-step sequential update
of an exploration issue structure based on the schema version of the input.
Copy link
Member

Choose a reason for hiding this comment

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

exploration issue structure --> exploration issue dict

based on the schema version of the input --> based on its schema version

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.


def _migrate_action_schema(learner_action_dict):
"""Holds the responsibility of performing a step-by-step sequential update
of an learner action structure based on the schema version of the input.
Copy link
Member

Choose a reason for hiding this comment

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

See above, similar comments apply

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.

@@ -31,6 +32,68 @@
VERSION_ALL = 'all'


def _migrate_issue_schema(exp_issue_dict):
Copy link
Member

Choose a reason for hiding this comment

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

_migrate_to_latest_issue_schema

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.

issue_schema_version += 1


def _migrate_action_schema(learner_action_dict):
Copy link
Member

Choose a reason for hiding this comment

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

ditto: naming (there should be a notion of "latest")

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

Almost there!

@seanlip
Copy link
Member

seanlip commented May 23, 2018 via email

@brianrodri
Copy link
Contributor

brianrodri commented May 23, 2018 via email

@brianrodri brianrodri merged commit f5426da into oppia:develop May 23, 2018
@pranavsid98 pranavsid98 deleted the store_playthrough_controller branch May 24, 2018 05:05
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.

4 participants