Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
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