Skip to content

Commit

Permalink
feat: created learner_skill_levels API to fetch skill scores (#31417)
Browse files Browse the repository at this point in the history
  • Loading branch information
sameenfatima78 authored Jan 4, 2023
1 parent 65da5b5 commit 4b6e719
Show file tree
Hide file tree
Showing 11 changed files with 787 additions and 0 deletions.
1 change: 1 addition & 0 deletions openedx/core/djangoapps/catalog/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ def get_course_data(course_key_str, fields):
Arguments:
course_key_str: key for the course about which we are retrieving information.
fields (List, string): The given fields that you want to retrieve from API response.
Returns:
dict with details about specified course.
Expand Down
Empty file.
31 changes: 31 additions & 0 deletions openedx/core/djangoapps/user_api/learner_skill_levels/api.py
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
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.
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
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
Loading

0 comments on commit 4b6e719

Please sign in to comment.