-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Playthrough Visualisation: Milestone 1.2 (Part 2) #4946
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
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 Pranav, I've taken my first pass! I see mostly stylistic issues, the logic looks good to me!
core/controllers/reader.py
Outdated
# either 'state_name' or 'state_names'. | ||
state_name_in_customization_args = ( | ||
True | ||
if 'state_name' in customization_args else False) |
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 can be simplified to
state_name_in_customization_args = 'state_name' in customization_args
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.
Ahh. right. Makes sense. Done.
core/controllers/reader.py
Outdated
customization_args['state_name']))) or ( | ||
not state_name_in_customization_args and ( | ||
issue_customization_args['state_names'] == ( | ||
customization_args['state_names']))): |
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 expression is rather hard to read, here are some ideas for improvement:
- Move it to a function with many simple
return True
orreturn False
statements. - Split the expression into more variables (like you've done with
state_name_in_customization_args
)
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.
Did the first point.
core/controllers/reader.py
Outdated
exp_issues.unresolved_issues.append(issue) | ||
|
||
stats_services.save_exp_issues_model_transactional( | ||
exp_issues) |
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 statement should fit on one line?
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.
Ah true. Done.
core/controllers/reader_test.py
Outdated
self.playthrough_data['issue_customization_args']['state_name'][ | ||
'value'] = 'state_name2' | ||
|
||
self.post_json('/explorehandler/store_playthrough/%s' % ( |
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: 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
})
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.
"""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.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
core/controllers/reader_test.py
Outdated
}) | ||
model.put() | ||
|
||
|
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: only use 2 blank lines between classes.
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/controllers/reader.py
Outdated
customization_args: dict. The customization dict for the issue of | ||
the playthrough to be stored. | ||
""" | ||
if state_name_present: |
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.
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]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed -- 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.
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.
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.
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.
@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.
core/controllers/reader.py
Outdated
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. |
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 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.
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.
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)
core/controllers/reader.py
Outdated
class StorePlaythroughHandler(base.BaseHandler): | ||
"""Handles a useful playthrough coming in from the frontend to store it.""" | ||
|
||
REQUIRE_PAYLOAD_CSRF_CHECK = False |
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.
Please add a comment to explain what this is/what it is used 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.
Actually @pranavsid98 this seems incorrect. We should have a CSRF check. Why bypass it?
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/controllers/reader.py
Outdated
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']) < ( |
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: move the parenthesis to wrap the entire expression:
if (len(issue['playthrough_ids']) <
feconf.MAX_PLAYTHROUGHS_FOR_ISSUE):
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/controllers/reader_test.py
Outdated
|
||
self.login(self.VIEWER_EMAIL) | ||
self.signup(self.VIEWER_EMAIL, self.VIEWER_USERNAME) | ||
exp_services.delete_demo(self.exp_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.
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
?
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.
Actually, its not required. Ive removed it.
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, please commit that then!
core/controllers/reader_test.py
Outdated
'value': 'state_name1' | ||
} | ||
} | ||
}], True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use keyword argument here so it's clearer "what" is going to be set to True
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
core/controllers/reader_test.py
Outdated
'value': 'state_name1' | ||
} | ||
} | ||
}], True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, please add keyword argument label here.
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/stats_domain.py
Outdated
@@ -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. |
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.
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.)
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.
@brianrodri @seanlip This is good to go for another pass. PTAL! Thanks :) |
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.
Excellent, looks good to me! Have a few more round of requested changes before we can merge :)
core/controllers/reader.py
Outdated
|
||
@acl_decorators.can_play_exploration | ||
def post(self, exploration_id): | ||
"""Handles POST requests. |
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.
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).
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/controllers/reader_test.py
Outdated
|
||
self.login(self.VIEWER_EMAIL) | ||
self.signup(self.VIEWER_EMAIL, self.VIEWER_USERNAME) | ||
exp_services.delete_demo(self.exp_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.
OK, please commit that then!
core/controllers/reader_test.py
Outdated
"""Test that a new playthrough gets created and a new issue is created | ||
for it. | ||
""" | ||
self.exp_issue['customization_args']['state_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.
nit: use parenthesis here since the indexing fits on one line:
self.exp_issue['customization_args']['state_name']['value'] = (
'state_name2')
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/stats_services.py
Outdated
ExplorationIssues domain object. | ||
|
||
Args: | ||
exp_issues. ExplorationIssues. The exploration issues 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.
should use a ":" here,
exp_issues: ExplorationIssues.
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/stats_services.py
Outdated
ExplorationIssues domain object in a transaction. | ||
|
||
Args: | ||
exp_issues. ExplorationIssues. The exploration issues 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.
Same here.
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.
Whoops, forgot to hit Request changes
@pranavsid98 FYI I'll take one more pass later this evening. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay, @pranavsid98. Took a careful pass, PTAL.
core/controllers/reader.py
Outdated
class StorePlaythroughHandler(base.BaseHandler): | ||
"""Handles a useful playthrough coming in from the frontend to store it.""" | ||
|
||
@acl_decorators.can_play_exploration |
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 "play"? Shouldn't it at least be edit access?
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 Nope. A playthrough of a learner is what's being stored right? The learner user doesnt need to have edit access 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.
Why do you want to allow learners to look at other learners' playthroughs?
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 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry! It's me who understood it wrongly. You're correct. Thank you for pushing back.
core/controllers/reader.py
Outdated
else: | ||
# In this case, the playthrough instance created doesn't | ||
# need to be stored, so it is deleted. | ||
stats_models.PlaythroughModel.get( |
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 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?
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.
True. I modified it to do create only when required instead.
core/controllers/reader.py
Outdated
|
||
issue_found = False | ||
for index, issue in enumerate(exp_issues.unresolved_issues): | ||
if issue['issue_type'] == exp_issue_dict['issue_type']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any guarantee that each issue in the list of unresolved issues has a different (type, key) tuple? If so, where is this enforced?
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 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?
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 as long as this is the only mechanism for adding new issues.
core/controllers/reader.py
Outdated
'issue_customization_args'], | ||
'playthrough_ids': [playthrough_id] | ||
} | ||
exp_issues.unresolved_issues.append(issue) |
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.
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?
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 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.
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, 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.
This comment was marked as off-topic.
Sorry, something went wrong.
core/domain/issue_registry.py
Outdated
@@ -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 |
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.
docstring says ids, this says types, please standardize.
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.
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) |
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.
Probably want a choices parameter too? See e.g. #4943.
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 Shouldn't we be trying to define all our issue/action types in feconf and use it from there? Is this actually required?
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 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.)
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.
# 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 |
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 be ndb.JsonProperty(repeated=True) to signify list
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.
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 |
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.
two lines below: bool not Bool.
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.
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 |
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 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.
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 Does it seem better now?
core/controllers/reader.py
Outdated
class StorePlaythroughHandler(base.BaseHandler): | ||
"""Handles a useful playthrough coming in from the frontend to store it.""" | ||
|
||
@acl_decorators.can_play_exploration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry! It's me who understood it wrongly. You're correct. Thank you for pushing back.
core/controllers/reader.py
Outdated
|
||
issue_found = False | ||
for index, issue in enumerate(exp_issues.unresolved_issues): | ||
if issue['issue_type'] == exp_issue_dict['issue_type']: |
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 as long as this is the only mechanism for adding new issues.
core/controllers/reader.py
Outdated
'issue_customization_args'], | ||
'playthrough_ids': [playthrough_id] | ||
} | ||
exp_issues.unresolved_issues.append(issue) |
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, 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) |
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 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) |
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 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.)
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/controllers/reader.py
Outdated
# 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 |
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: 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.
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.
@@ -1044,6 +1044,26 @@ def create( | |||
playthrough_instance.put() | |||
return instance_id | |||
|
|||
@classmethod | |||
def create_from_dict(cls, playthrough_data): |
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.
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.
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.
extensions/actions/base_test.py
Outdated
EXPLORATION_START_ID = 'ExplorationStart' | ||
ANSWER_SUBMIT_ID = 'AnswerSubmit' | ||
EXPLORATION_QUIT_ID = 'ExplorationQuit' | ||
EXPLORATION_START_TYPE = 'ExplorationStart' |
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.
ACTION_TYPE_EXPLORATION_START
ACTION_TYPE_ANSWER_SUBMIT
ACTION_TYPE_EXPLORATION_QUIT
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.
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.
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.
extensions/issues/base_test.py
Outdated
EARLY_QUIT_ID = 'EarlyQuit' | ||
MULTIPLE_INCORRECT_SUBMISSIONS_ID = 'MultipleIncorrectSubmissions' | ||
CYCLIC_STATE_TRANSITIONS_ID = 'CyclicStateTransitions' | ||
EARLY_QUIT_TYPE = 'EarlyQuit' |
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.
ISSUE_TYPE_EARLY_QUIT, etc.
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.
feconf.py
Outdated
# IDs of allowed types of issues. | ||
ALLOWED_ISSUE_IDS = [ | ||
# Types of allowed issues. | ||
ALLOWED_ISSUE_TYPES = [ | ||
'EarlyQuit', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you 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.
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.
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). |
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.
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.
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.
@pranavsid98 I don't see any new changes to review. Did you push your commits? |
core/controllers/reader.py
Outdated
for playthrough_property in playthrough_properties: | ||
if playthrough_property not in playthrough_data: | ||
raise self.InvalidInputException( | ||
'%s not in playthrough data dict.' % (playthrough_property)) |
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 need parens around playthrough_property
core/controllers/reader.py
Outdated
playthrough_data['issue_type'], | ||
playthrough_data['issue_customization_args'], | ||
playthrough_data['playthrough_actions'], | ||
playthrough_data['is_valid']) | ||
issue = { |
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 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.
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. 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) |
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 do you need a default value at all here? None is never valid, 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.
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.
Looks good on the whole, only one issue with schema versions I think.
core/controllers/reader_test.py
Outdated
@@ -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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here and below, I thought you were removing the schema_version key from the stuff that gets passed in?
core/domain/stats_domain.py
Outdated
""" | ||
self.issue_type = issue_type | ||
self.schema_version = schema_version | ||
self.customization_args = customization_args | ||
self.schema_version = stats_models.CURRENT_ISSUE_SCHEMA_VERSION |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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/stats_domain.py
Outdated
'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( |
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.
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.
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.
Filed a followup issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(For record-keeping: please refer to the follow-up issue number in your comment.)
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.
Note: field as #4968.
core/domain/stats_domain.py
Outdated
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 |
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.
ditto
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.
Almost there!
core/controllers/reader.py
Outdated
@@ -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, |
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 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.
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 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.
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/controllers/reader.py
Outdated
@@ -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, |
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.
Ditto, don't just assume current schema version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
core/controllers/reader_test.py
Outdated
@@ -1362,7 +1366,6 @@ def test_playthrough_gets_added_to_cyclic_issues(self): | |||
|
|||
self.exp_issue_dict = { | |||
'issue_type': 'CyclicStateTransitions', | |||
'schema_version': 1, |
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.
Looks like you've now deleted this here and added it everywhere else ...
Why isn't this getting caught as a problem?
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 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.
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/controllers/reader_test.py
Outdated
@@ -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, |
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.
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).
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/stats_domain.py
Outdated
@@ -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, |
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.
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.)
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 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?
core/domain/stats_domain_test.py
Outdated
'issue_customization_args': expected_customization_args, | ||
'playthrough_ids': [] | ||
}) | ||
|
||
def test_update_exp_issue_from_model(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.
This is a really well-written test! Very nice.
core/domain/stats_services.py
Outdated
@@ -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. |
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.
exploration issue structure --> exploration issue dict
based on the schema version of the input --> based on its schema version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
core/domain/stats_services.py
Outdated
|
||
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. |
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.
See above, similar comments apply
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/stats_services.py
Outdated
@@ -31,6 +32,68 @@ | |||
VERSION_ALL = 'all' | |||
|
|||
|
|||
def _migrate_issue_schema(exp_issue_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.
_migrate_to_latest_issue_schema
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/stats_services.py
Outdated
issue_schema_version += 1 | ||
|
||
|
||
def _migrate_action_schema(learner_action_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.
ditto: naming (there should be a notion of "latest")
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.
Almost there!
Hi Pranav -- the GitHub page is down, so I can't actually look at your PR,
but I was able to review the individual commit
<0f19145> and I left two suggestions.
They're nits, so basically this PR LGTM.
Except that I can't actually approve it, because the GitHub PR page is
down. I have sent a support request and will let you know if this changes.
|
Hmm nope, I'm getting the Unicorn! page as well
…On Wed, May 23, 2018 at 2:44 PM Sean Lip ***@***.***> wrote:
Hi Pranav -- the GitHub page is down, so I can't actually look at your PR,
but I was able to review the individual commit
<0f19145> and I left two
suggestions.
They're nits, so basically this PR LGTM.
Except that I can't actually approve it, because the GitHub PR page is
down. I have sent a support request and will let you know if this changes.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#4946 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AE26rOlbGzpktFT1LJn_P01XBZMVVtL6ks5t1a4VgaJpZM4UBWX2>
.
|
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:
@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