-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: created learner_skill_levels API to fetch skill scores (#31417)
- Loading branch information
1 parent
65da5b5
commit 4b6e719
Showing
11 changed files
with
787 additions
and
0 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
Empty file.
31 changes: 31 additions & 0 deletions
31
openedx/core/djangoapps/user_api/learner_skill_levels/api.py
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,31 @@ | ||
""" | ||
APIs for learner skill levels. | ||
""" | ||
from .utils import get_skills_score, calculate_user_skill_score, generate_skill_score_mapping | ||
|
||
|
||
def get_learner_skill_levels(user, top_categories): | ||
""" | ||
Evaluates learner's skill levels in the given job category. Only considers skills for the categories | ||
and not their sub-categories. | ||
Params: | ||
user: user for each score is being calculated. | ||
top_categories (List, string): A list of fields (as strings) of job categories and their skills. | ||
Returns: | ||
top_categories: Categories with scores appended to skills. | ||
""" | ||
|
||
# get a skill to score mapping for every course user has passed | ||
skill_score_mapping = generate_skill_score_mapping(user) | ||
for skill_category in top_categories: | ||
category_skills = skill_category['skills'] | ||
get_skills_score(category_skills, skill_score_mapping) | ||
skill_category['user_score'] = calculate_user_skill_score(category_skills) | ||
skill_category['edx_average_score'] = None | ||
sub_categories = skill_category['skills_subcategories'] | ||
for sub_category in sub_categories: | ||
subcategory_skills = sub_category['skills'] | ||
get_skills_score(subcategory_skills, skill_score_mapping) | ||
|
||
return top_categories |
8 changes: 8 additions & 0 deletions
8
openedx/core/djangoapps/user_api/learner_skill_levels/constants.py
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,8 @@ | ||
""" | ||
Constants for learner skill levels app. | ||
""" | ||
LEVEL_TYPE_SCORE_MAPPING = { | ||
'Introductory': 1, | ||
'Intermediate': 2, | ||
'Advanced': 3 | ||
} |
Empty file.
216 changes: 216 additions & 0 deletions
216
openedx/core/djangoapps/user_api/learner_skill_levels/tests/test_utils.py
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,216 @@ | ||
""" Unit tests for Learner Skill Levels utilities. """ | ||
|
||
import ddt | ||
from collections import defaultdict | ||
from unittest import mock | ||
|
||
from rest_framework.test import APIClient | ||
|
||
from common.djangoapps.student.tests.factories import UserFactory | ||
|
||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin | ||
from openedx.core.djangoapps.user_api.learner_skill_levels.utils import ( | ||
calculate_user_skill_score, | ||
generate_skill_score_mapping, | ||
get_base_url, | ||
get_job_holder_usernames, | ||
get_skills_score, | ||
get_top_skill_categories_for_job, | ||
update_category_user_scores_map, | ||
update_edx_average_score, | ||
) | ||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase | ||
|
||
from .testutils import ( | ||
DUMMY_CATEGORIES_RESPONSE, | ||
DUMMY_CATEGORIES_WITH_SCORES, | ||
DUMMY_USERNAMES_RESPONSE, | ||
DUMMY_COURSE_DATA_RESPONSE, | ||
DUMMY_USER_SCORES_MAP, | ||
) | ||
|
||
|
||
@ddt.ddt | ||
class LearnerSkillLevelsUtilsTests(SharedModuleStoreTestCase, CatalogIntegrationMixin): | ||
""" | ||
Test LearnerSkillLevel utilities. | ||
""" | ||
SERVICE_USERNAME = 'catalog_service_username' | ||
|
||
def setUp(self): | ||
""" | ||
Unit tests setup. | ||
""" | ||
super().setUp() | ||
|
||
self.client = APIClient() | ||
self.service_user = UserFactory(username=self.SERVICE_USERNAME) | ||
self.catalog_integration = self.create_catalog_integration() | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_course_run_ids') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_course_data') | ||
def test_generate_skill_score_mapping( | ||
self, | ||
mock_get_course_data, | ||
mock_get_course_run_ids, | ||
): | ||
""" | ||
Test that skill-score mapping is returned in correct format. | ||
""" | ||
user = UserFactory(username='edX') | ||
mock_get_course_run_ids.return_value = ['AWS+OTP-AWSD12'] | ||
mock_get_course_data.return_value = DUMMY_COURSE_DATA_RESPONSE | ||
result = generate_skill_score_mapping(user) | ||
expected_response = {"python": 3, "MongoDB": 3, "Data Science": 3} | ||
assert result == expected_response | ||
|
||
@ddt.data( | ||
([], 0.0), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": None}, | ||
{"id": 2, "name": "Fintech", "score": None}, | ||
], 0.0 | ||
), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": None}, | ||
{"id": 2, "name": "Fintech", "score": None}, | ||
], 0.0 | ||
), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": 3}, | ||
{"id": 2, "name": "Fintech", "score": 2}, | ||
], 0.8 | ||
), | ||
) | ||
@ddt.unpack | ||
def test_calculate_user_skill_score(self, skills_with_score, expected): | ||
""" | ||
Test that skill-score mapping is returned in correct format. | ||
""" | ||
|
||
result = calculate_user_skill_score(skills_with_score) | ||
assert result == expected | ||
|
||
@ddt.data( | ||
([], {"Financial Management": 1, "Fintech": 3}, []), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management"}, | ||
{"id": 2, "name": "Fintech"}, | ||
], | ||
{ | ||
"Financial Management": 1, | ||
"Fintech": 3 | ||
}, | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": 1}, | ||
{"id": 2, "name": "Fintech", "score": 3}, | ||
], | ||
), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management"}, | ||
{"id": 2, "name": "Fintech"}, | ||
], | ||
{}, | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": None}, | ||
{"id": 2, "name": "Fintech", "score": None}, | ||
], | ||
), | ||
( | ||
[ | ||
{"id": 1, "name": "Financial Management"}, | ||
{"id": 2, "name": "Fintech"}, | ||
], | ||
{ | ||
"Python": 1, | ||
"AI": 3 | ||
}, | ||
[ | ||
{"id": 1, "name": "Financial Management", "score": None}, | ||
{"id": 2, "name": "Fintech", "score": None}, | ||
], | ||
), | ||
) | ||
@ddt.unpack | ||
def test_get_skills_score(self, skills, learner_skill_score, expected): | ||
""" | ||
Test that skill-score mapping is returned in correct format. | ||
""" | ||
get_skills_score(skills, learner_skill_score) | ||
assert skills == expected | ||
|
||
def test_update_category_user_scores_map(self): | ||
""" | ||
Test that skill-score mapping is returned in correct format. | ||
""" | ||
category_user_scores_map = defaultdict(list) | ||
update_category_user_scores_map(DUMMY_CATEGORIES_WITH_SCORES["skill_categories"], category_user_scores_map) | ||
expected = {"Information Technology": [0.8], "Finance": [0.3]} | ||
assert category_user_scores_map == expected | ||
|
||
def test_update_edx_average_score(self): | ||
""" | ||
Test that skill-score mapping is returned in correct format. | ||
""" | ||
update_edx_average_score(DUMMY_CATEGORIES_WITH_SCORES["skill_categories"], DUMMY_USER_SCORES_MAP) | ||
assert DUMMY_CATEGORIES_WITH_SCORES["skill_categories"][0]["edx_average_score"] == 0.4 | ||
assert DUMMY_CATEGORIES_WITH_SCORES["skill_categories"][1]["edx_average_score"] == 0.5 | ||
|
||
@ddt.data( | ||
("http://localhost:18000/api/", "http://localhost:18000"), | ||
("http://localhost:18000/", "http://localhost:18000"), | ||
) | ||
@ddt.unpack | ||
def test_get_base_url(self, source_url, expected): | ||
""" | ||
Test that base url is returned correctly. | ||
""" | ||
actual = get_base_url(source_url) | ||
assert actual == expected | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_catalog_api_client') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_catalog_api_base_url') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_api_data') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.check_catalog_integration_and_get_user') | ||
def test_get_top_skill_categories_for_job( | ||
self, | ||
mock_check_catalog_integration_and_get_user, | ||
mock_get_api_data, | ||
mock_get_catalog_api_base_url, | ||
mock_get_catalog_api_client | ||
): | ||
""" | ||
Test that get_top_skill_categories_for_job returns jobs categories. | ||
""" | ||
mock_check_catalog_integration_and_get_user.return_value = self.service_user, self.catalog_integration | ||
mock_get_api_data.return_value = DUMMY_CATEGORIES_RESPONSE | ||
mock_get_catalog_api_base_url.return_value = 'localhost:18381/api' | ||
mock_get_catalog_api_client.return_value = self.client | ||
result = get_top_skill_categories_for_job(1) | ||
assert result == DUMMY_CATEGORIES_RESPONSE | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_catalog_api_client') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_catalog_api_base_url') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.get_api_data') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.utils.check_catalog_integration_and_get_user') | ||
def test_get_job_holder_usernames( | ||
self, | ||
mock_check_catalog_integration_and_get_user, | ||
mock_get_api_data, | ||
mock_get_catalog_api_base_url, | ||
mock_get_catalog_api_client | ||
): | ||
""" | ||
Test that test_get_job_holder_usernames returns usernames. | ||
""" | ||
mock_check_catalog_integration_and_get_user.return_value = self.service_user, self.catalog_integration | ||
mock_get_api_data.return_value = DUMMY_USERNAMES_RESPONSE | ||
mock_get_catalog_api_base_url.return_value = 'localhost:18381/api' | ||
mock_get_catalog_api_client.return_value = self.client | ||
result = get_job_holder_usernames(1) | ||
assert result == DUMMY_USERNAMES_RESPONSE |
119 changes: 119 additions & 0 deletions
119
openedx/core/djangoapps/user_api/learner_skill_levels/tests/test_views.py
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,119 @@ | ||
""" | ||
Test cases for LearnerSkillLevelsView. | ||
""" | ||
|
||
from unittest import mock | ||
|
||
from django.urls import reverse | ||
from rest_framework.test import APIClient, APITestCase | ||
|
||
from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory | ||
|
||
from .testutils import DUMMY_CATEGORIES_RESPONSE, DUMMY_USERNAMES_RESPONSE | ||
|
||
|
||
class LearnerSkillLevelsViewTests(APITestCase): | ||
""" | ||
The tests for LearnerSkillLevelsView. | ||
""" | ||
|
||
def setUp(self): | ||
super().setUp() | ||
|
||
self.client = APIClient() | ||
self.user = UserFactory.create(password=TEST_PASSWORD) | ||
self.url = reverse('learner_skill_level', kwargs={'job_id': '1'}) | ||
|
||
for username in DUMMY_USERNAMES_RESPONSE['usernames']: | ||
UserFactory(username=username) | ||
|
||
def test_unauthorized_get_endpoint(self): | ||
""" | ||
Test that endpoint is only accessible to authorized user. | ||
""" | ||
response = self.client.get(self.url) | ||
assert response.status_code == 401 | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_top_skill_categories_for_job') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_job_holder_usernames') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.api.generate_skill_score_mapping') | ||
def test_get_endpoint( | ||
self, | ||
mock_generate_skill_score_mapping, | ||
mock_get_job_holder_usernames, | ||
mock_get_top_skill_categories_for_job | ||
): | ||
""" | ||
Test that response if returned with correct scores appended. | ||
""" | ||
mock_get_top_skill_categories_for_job.return_value = DUMMY_CATEGORIES_RESPONSE | ||
mock_get_job_holder_usernames.return_value = DUMMY_USERNAMES_RESPONSE | ||
mock_generate_skill_score_mapping.return_value = {'Technology Roadmap': 2, 'Python': 3} | ||
|
||
self.client.login(username=self.user.username, password=TEST_PASSWORD) | ||
response = self.client.get(self.url) | ||
assert response.status_code == 200 | ||
# check if the response is mutated and scores are appended for skills | ||
# for when some skills are learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][0]['user_score'] == 0.8 | ||
assert response.data['skill_categories'][0]['edx_average_score'] == 0.8 | ||
|
||
# for when no skill is learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][1]['user_score'] == 0.0 | ||
assert response.data['skill_categories'][1]['edx_average_score'] == 0.0 | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_top_skill_categories_for_job') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_job_holder_usernames') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.api.generate_skill_score_mapping') | ||
def test_get_with_less_than_5_users( | ||
self, | ||
mock_generate_skill_score_mapping, | ||
mock_get_job_holder_usernames, | ||
mock_get_top_skill_categories_for_job | ||
): | ||
""" | ||
Test that average value is None when users are less than 5. | ||
""" | ||
mock_get_top_skill_categories_for_job.return_value = DUMMY_CATEGORIES_RESPONSE | ||
mock_get_job_holder_usernames.return_value = {"usernames": ['user1', 'user2']} | ||
mock_generate_skill_score_mapping.return_value = {'Technology Roadmap': 2, 'Python': 3} | ||
|
||
self.client.login(username=self.user.username, password=TEST_PASSWORD) | ||
response = self.client.get(self.url) | ||
assert response.status_code == 200 | ||
# check if the response is mutated and scores are appended for skills | ||
# for when some skills are learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][0]['user_score'] == 0.8 | ||
assert response.data['skill_categories'][0]['edx_average_score'] is None | ||
|
||
# for when no skill is learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][1]['user_score'] == 0.0 | ||
assert response.data['skill_categories'][1]['edx_average_score'] is None | ||
|
||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_top_skill_categories_for_job') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.views.get_job_holder_usernames') | ||
@mock.patch('openedx.core.djangoapps.user_api.learner_skill_levels.api.generate_skill_score_mapping') | ||
def test_get_no_skills_learned( | ||
self, | ||
mock_generate_skill_score_mapping, | ||
mock_get_job_holder_usernames, | ||
mock_get_top_skill_categories_for_job | ||
): | ||
""" | ||
Test that score is 0.0 when no skills are learned by a user. | ||
""" | ||
mock_get_top_skill_categories_for_job.return_value = DUMMY_CATEGORIES_RESPONSE | ||
mock_get_job_holder_usernames.return_value = DUMMY_USERNAMES_RESPONSE | ||
mock_generate_skill_score_mapping.return_value = {} | ||
|
||
self.client.login(username=self.user.username, password=TEST_PASSWORD) | ||
response = self.client.get(self.url) | ||
assert response.status_code == 200 | ||
# check if the response is mutated and scores are appended for skills | ||
# for when some skills are learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][0]['user_score'] == 0.0 | ||
assert response.data['skill_categories'][0]['edx_average_score'] == 0.0 | ||
|
||
# for when no skill is learned by user in a category, check if user_score and avg score is appended | ||
assert response.data['skill_categories'][1]['user_score'] == 0.0 | ||
assert response.data['skill_categories'][1]['edx_average_score'] == 0.0 |
Oops, something went wrong.