Skip to content

Commit

Permalink
Fix oppia#7017: Backend changes for UserSkillMastery and add a handle…
Browse files Browse the repository at this point in the history
…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
sophiewu6 authored and seanlip committed Jul 30, 2019
1 parent d6bdca8 commit 4eb10c8
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 20 deletions.
80 changes: 80 additions & 0 deletions core/controllers/skill_mastery.py
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({})
197 changes: 197 additions & 0 deletions core/controllers/skill_mastery_test.py
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.')
52 changes: 40 additions & 12 deletions core/domain/skill_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,27 @@ def save_user_skill_mastery(user_skill_mastery):
user_skill_mastery_model.put()


def get_skill_mastery(user_id, skill_id):
def create_multi_user_skill_mastery(user_id, degrees_of_mastery):
"""Creates the mastery of a user in multiple skills.
Args:
user_id: str. The user ID of the user.
degrees_of_mastery: dict(str, float). The keys are the requested
skill IDs. The values are the corresponding mastery degree of
the user.
"""
user_skill_mastery_models = []

for skill_id, degree_of_mastery in degrees_of_mastery.iteritems():
user_skill_mastery_models.append(user_models.UserSkillMasteryModel(
id=user_models.UserSkillMasteryModel.construct_model_id(
user_id, skill_id),
user_id=user_id, skill_id=skill_id,
degree_of_mastery=degree_of_mastery))
user_models.UserSkillMasteryModel.put_multi(user_skill_mastery_models)


def get_user_skill_mastery(user_id, skill_id):
"""Fetches the mastery of user in a particular skill.
Args:
Expand All @@ -848,18 +868,21 @@ def get_skill_mastery(user_id, skill_id):
requested.
Returns:
degree_of_mastery: float. Mastery degree of the user for the
requested skill.
degree_of_mastery: float or None. Mastery degree of the user for the
requested skill, or None if UserSkillMasteryModel does not exist
for the skill.
"""
model_id = user_models.UserSkillMasteryModel.construct_model_id(
user_id, skill_id)
degree_of_mastery = user_models.UserSkillMasteryModel.get(
model_id).degree_of_mastery
user_skill_mastery_model = user_models.UserSkillMasteryModel.get(
model_id, strict=False)

return degree_of_mastery
if not user_skill_mastery_model:
return None
return user_skill_mastery_model.degree_of_mastery


def get_multi_skill_mastery(user_id, skill_ids):
def get_multi_user_skill_mastery(user_id, skill_ids):
"""Fetches the mastery of user in multiple skills.
Args:
Expand All @@ -868,10 +891,12 @@ def get_multi_skill_mastery(user_id, skill_ids):
requested.
Returns:
degree_of_mastery: list(float). Mastery degree of the user for requested
skills.
degrees_of_mastery: dict(str, float|None). The keys are the requested
skill IDs. The values are the corresponding mastery degree of
the user or None if UserSkillMasteryModel does not exist for the
skill.
"""
degrees_of_mastery = []
degrees_of_mastery = {}
model_ids = []

for skill_id in skill_ids:
Expand All @@ -881,8 +906,11 @@ def get_multi_skill_mastery(user_id, skill_ids):
skill_mastery_models = user_models.UserSkillMasteryModel.get_multi(
model_ids)

for skill_mastery_model in skill_mastery_models:
degrees_of_mastery.append(skill_mastery_model.degree_of_mastery)
for skill_id, skill_mastery_model in zip(skill_ids, skill_mastery_models):
if skill_mastery_model is None:
degrees_of_mastery[skill_id] = None
else:
degrees_of_mastery[skill_id] = skill_mastery_model.degree_of_mastery

return degrees_of_mastery

Expand Down
Loading

0 comments on commit 4eb10c8

Please sign in to comment.