forked from oppia/oppia
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix oppia#7017: Backend changes for UserSkillMastery and add a handle…
…r for updating user skill mastery degree (oppia#7193) * user skill mastery backend * lint * review changes * lint * review changes * lint * review changes * lint * edit docstring * review changes * changed file name * review changes * review changes
- Loading branch information
Showing
6 changed files
with
352 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# Copyright 2019 The Oppia Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS-IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Controllers for the skill mastery.""" | ||
|
||
from core.controllers import acl_decorators | ||
from core.controllers import base | ||
from core.domain import skill_domain | ||
from core.domain import skill_services | ||
import utils | ||
|
||
|
||
class SkillMasteryDataHandler(base.BaseHandler): | ||
"""A handler that handles fetching and updating the degrees of user | ||
skill mastery. | ||
""" | ||
|
||
@acl_decorators.can_access_learner_dashboard | ||
def put(self): | ||
"""Handles PUT requests.""" | ||
degree_of_mastery_per_skill = ( | ||
self.payload.get('degree_of_mastery_per_skill')) | ||
if (not degree_of_mastery_per_skill or | ||
not isinstance(degree_of_mastery_per_skill, dict)): | ||
raise self.InvalidInputException( | ||
'Expected payload to contain degree_of_mastery_per_skill ' | ||
'as a dict.') | ||
|
||
skill_ids = degree_of_mastery_per_skill.keys() | ||
|
||
for skill_id in skill_ids: | ||
try: | ||
skill_domain.Skill.require_valid_skill_id(skill_id) | ||
except utils.ValidationError: | ||
raise self.InvalidInputException( | ||
'Invalid skill ID %s' % skill_id) | ||
|
||
# float(bool) will not raise an error. | ||
if isinstance(degree_of_mastery_per_skill[skill_id], bool): | ||
raise self.InvalidInputException( | ||
'Expected degree of mastery of skill %s to be a number, ' | ||
'received %s.' | ||
% (skill_id, degree_of_mastery_per_skill[skill_id])) | ||
|
||
try: | ||
degree_of_mastery_per_skill[skill_id] = ( | ||
float(degree_of_mastery_per_skill[skill_id])) | ||
except (TypeError, ValueError): | ||
raise self.InvalidInputException( | ||
'Expected degree of mastery of skill %s to be a number, ' | ||
'received %s.' | ||
% (skill_id, degree_of_mastery_per_skill[skill_id])) | ||
|
||
if (degree_of_mastery_per_skill[skill_id] < 0.0 or | ||
degree_of_mastery_per_skill[skill_id] > 1.0): | ||
raise self.InvalidInputException( | ||
'Expected degree of mastery of skill %s to be a float ' | ||
'between 0.0 and 1.0, received %s.' | ||
% (skill_id, degree_of_mastery_per_skill[skill_id])) | ||
|
||
try: | ||
skill_services.get_multi_skills(skill_ids) | ||
except Exception as e: | ||
raise self.PageNotFoundException(e) | ||
|
||
skill_services.create_multi_user_skill_mastery( | ||
self.user_id, degree_of_mastery_per_skill) | ||
|
||
self.render_json({}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
# Copyright 2019 The Oppia Authors. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS-IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Tests for the Question Player controller.""" | ||
|
||
from core.domain import skill_services | ||
from core.tests import test_utils | ||
import feconf | ||
|
||
|
||
class SkillMasteryDataHandlerTest(test_utils.GenericTestBase): | ||
"""Tests update skill mastery degree.""" | ||
|
||
def setUp(self): | ||
"""Completes the setup for SkillMasteryDataHandler.""" | ||
super(SkillMasteryDataHandlerTest, self).setUp() | ||
self.signup(self.NEW_USER_EMAIL, self.NEW_USER_USERNAME) | ||
self.user_id = self.get_user_id_from_email(self.NEW_USER_EMAIL) | ||
|
||
self.skill_id_1 = skill_services.get_new_skill_id() | ||
self.save_new_skill( | ||
self.skill_id_1, self.user_id, 'Skill Description 1') | ||
self.skill_id_2 = skill_services.get_new_skill_id() | ||
self.save_new_skill( | ||
self.skill_id_2, self.user_id, 'Skill Description 2') | ||
|
||
def test_put_with_valid_skill_mastery_dict(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: 0.3, | ||
self.skill_id_2: 0.5 | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token) | ||
|
||
self.logout() | ||
|
||
def test_put_with_invalid_type_returns_400(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = [self.skill_id_1, self.skill_id_2] | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'Expected payload to contain degree_of_mastery_per_skill as a dict.' | ||
) | ||
|
||
self.logout() | ||
|
||
def test_put_with_no_degree_of_mastery_per_skill_returns_400(self): | ||
payload = {} | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'Expected payload to contain degree_of_mastery_per_skill as a dict.' | ||
) | ||
|
||
self.logout() | ||
|
||
def test_put_with_invalid_skill_ids_returns_400(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
'invalid_skill_id': 0.3 | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], 'Invalid skill ID invalid_skill_id') | ||
|
||
self.logout() | ||
|
||
def test_put_with_nonexistent_skill_ids_returns_404(self): | ||
skill_id_3 = skill_services.get_new_skill_id() | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: 0.3, | ||
self.skill_id_2: 0.5, | ||
skill_id_3: 0.6 | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=404) | ||
|
||
self.logout() | ||
|
||
def test_put_with_invalid_type_of_degree_of_mastery_returns_400(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: 0.1, | ||
self.skill_id_2: {} | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'Expected degree of mastery of skill %s to be a number, ' | ||
'received %s.' % (self.skill_id_2, '{}')) | ||
|
||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: 0.1, | ||
self.skill_id_2: True | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'Expected degree of mastery of skill %s to be a number, ' | ||
'received %s.' % (self.skill_id_2, 'True')) | ||
|
||
self.logout() | ||
|
||
def test_put_with_invalid_value_of_degree_of_mastery_returns_400(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: -0.4, | ||
self.skill_id_2: 0.5 | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
self.login(self.NEW_USER_EMAIL) | ||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=400) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'Expected degree of mastery of skill %s to be a float ' | ||
'between 0.0 and 1.0, received %s.' | ||
% (self.skill_id_1, '-0.4')) | ||
|
||
self.logout() | ||
|
||
def test_put_with_no_logged_in_user_returns_401(self): | ||
payload = {} | ||
degree_of_mastery_per_skill = { | ||
self.skill_id_1: 0.3, | ||
self.skill_id_2: 0.5 | ||
} | ||
payload['degree_of_mastery_per_skill'] = degree_of_mastery_per_skill | ||
|
||
csrf_token = self.get_new_csrf_token() | ||
json_response = self.put_json( | ||
'%s' % feconf.SKILL_MASTERY_DATA_URL, | ||
payload, csrf_token=csrf_token, expected_status_int=401) | ||
|
||
self.assertEqual( | ||
json_response['error'], | ||
'You must be logged in to access this resource.') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.