feat!: remove skill_levels API (#35863)
Resolves issue #35302 (https://github.com/openedx/edx-platform/issues/35302).
This commit is contained in:
@@ -9291,21 +9291,6 @@ paths:
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/user/v1/skill_level/{job_id}/:
|
||||
get:
|
||||
operationId: user_v1_skill_level_read
|
||||
description: GET /api/user/v1/skill_level/{job_id}/
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: job_id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/user/v1/user_prefs/:
|
||||
get:
|
||||
operationId: user_v1_user_prefs_list
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
Constants for learner skill levels app.
|
||||
"""
|
||||
LEVEL_TYPE_SCORE_MAPPING = {
|
||||
'Introductory': 1,
|
||||
'Intermediate': 2,
|
||||
'Advanced': 3
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
""" 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_run_data')
|
||||
@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_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 = ['course-v1:AWS+OTP-AWSD12']
|
||||
mock_get_course_run_data.return_value = {'course': 'AWS+OTP'}
|
||||
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
|
||||
@@ -1,119 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,110 +0,0 @@
|
||||
"""
|
||||
Utilities for unit tests of learner skill levels.
|
||||
"""
|
||||
|
||||
DUMMY_CATEGORIES_RESPONSE = {
|
||||
"job": "Digital Product Manager",
|
||||
"skill_categories": [
|
||||
{
|
||||
"name": "Information Technology",
|
||||
"id": 1,
|
||||
"skills": [
|
||||
{"id": 3, "name": "Technology Roadmap"},
|
||||
{"id": 12, "name": "Python"},
|
||||
{"id": 2, "name": "MongoDB"}
|
||||
],
|
||||
"skills_subcategories": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Databases",
|
||||
"skills": [
|
||||
{"id": 1, "name": "Query Languages"},
|
||||
{"id": 2, "name": "MongoDB"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "IT Management",
|
||||
"skills": [
|
||||
{"id": 3, "name": "Technology Roadmap"},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Finance",
|
||||
"id": 2,
|
||||
"skills": [
|
||||
{"id": 4, "name": "Accounting"},
|
||||
{"id": 5, "name": "TQM"},
|
||||
],
|
||||
"skills_subcategories": [
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Auditing",
|
||||
"skills": [
|
||||
{"id": 4, "name": "Accounting"},
|
||||
{"id": 5, "name": "TQM"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Management",
|
||||
"skills": [
|
||||
{"id": 6, "name": "Financial Management"},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
DUMMY_CATEGORIES_WITH_SCORES = {
|
||||
"job": "Digital Product Manager",
|
||||
"skill_categories": [
|
||||
{
|
||||
"name": "Information Technology",
|
||||
"id": 1,
|
||||
"skills": [
|
||||
{"id": 3, "name": "Technology Roadmap", "score": 1},
|
||||
{"id": 12, "name": "Python", "score": 2},
|
||||
{"id": 2, "name": "MongoDB", "score": 3}
|
||||
],
|
||||
"user_score": 0.8,
|
||||
},
|
||||
{
|
||||
"name": "Finance",
|
||||
"id": 2,
|
||||
"skills": [
|
||||
{"id": 1, "name": "Query Languages", "score": 1},
|
||||
{"id": 4, "name": "System Design", "score": 2},
|
||||
],
|
||||
"user_score": 0.3,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
DUMMY_USER_SCORES_MAP = {
|
||||
"Information Technology": [0.1, 0.3, 0.5, 0.7],
|
||||
"Finance": [0.2, 0.4, 0.6, 0.8]
|
||||
|
||||
}
|
||||
|
||||
DUMMY_USERNAMES_RESPONSE = {
|
||||
"usernames": [
|
||||
'test_user_1',
|
||||
'test_user_2',
|
||||
'test_user_3',
|
||||
'test_user_4',
|
||||
'test_user_5',
|
||||
'test_user_6',
|
||||
]
|
||||
}
|
||||
|
||||
DUMMY_COURSE_DATA_RESPONSE = {
|
||||
"key": "AWS+OTP",
|
||||
"uuid": "fe1a9ad4-a452-45cd-80e5-9babd3d43f96",
|
||||
"title": "Demonstration Course",
|
||||
"level_type": 'Advanced',
|
||||
"skill_names": ["python", "MongoDB", "Data Science"]
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
"""
|
||||
Utilities for learner_skill_levels.
|
||||
"""
|
||||
from logging import getLogger
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from lms.djangoapps.grades.models import PersistentCourseGrade # lint-amnesty, pylint: disable=unused-import
|
||||
from openedx.core.djangoapps.catalog.utils import (
|
||||
get_catalog_api_client,
|
||||
check_catalog_integration_and_get_user,
|
||||
get_catalog_api_base_url,
|
||||
|
||||
)
|
||||
from openedx.core.djangoapps.catalog.utils import get_course_data, get_course_run_data
|
||||
from openedx.core.lib.edx_api_utils import get_api_data
|
||||
|
||||
from .constants import LEVEL_TYPE_SCORE_MAPPING
|
||||
|
||||
LOGGER = getLogger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def get_course_run_ids(user):
|
||||
"""
|
||||
Returns all the course run ids of the courses that user has passed from PersistentCourseGrade model.
|
||||
"""
|
||||
return list(
|
||||
PersistentCourseGrade.objects.filter(
|
||||
user_id=user.id,
|
||||
passed_timestamp__isnull=False
|
||||
).values_list('course_id', flat=True)
|
||||
)
|
||||
|
||||
|
||||
def generate_skill_score_mapping(user):
|
||||
"""
|
||||
Generates a skill to score mapping for all the skills user has learner so far in passed courses.
|
||||
"""
|
||||
# get course_run_ids of all courses the user has passed
|
||||
course_run_ids = get_course_run_ids(user)
|
||||
|
||||
skill_score_mapping = {}
|
||||
for course_run_id in course_run_ids:
|
||||
# fetch course details from course run id to get course key
|
||||
course_run_data = get_course_run_data(course_run_id, ['course'])
|
||||
if course_run_data:
|
||||
# fetch course details to get level type and skills
|
||||
course_data = get_course_data(course_run_data['course'], ['skill_names', 'level_type'])
|
||||
skill_names = course_data['skill_names']
|
||||
level_type = course_data['level_type']
|
||||
|
||||
# if a level_type is None for a course, we should skip that course.
|
||||
if level_type:
|
||||
score = LEVEL_TYPE_SCORE_MAPPING[level_type.capitalize()]
|
||||
for skill in skill_names:
|
||||
if skill in skill_score_mapping:
|
||||
# assign scores b/w 1-3 based on level type
|
||||
# assign the larger score if skill is repeated in 2 courses
|
||||
skill_score_mapping[skill] = max(score, skill_score_mapping[skill])
|
||||
else:
|
||||
skill_score_mapping.update({skill: score})
|
||||
LOGGER.info(
|
||||
"Could not find course_key for course run id [%s].", course_run_id
|
||||
)
|
||||
return skill_score_mapping
|
||||
|
||||
|
||||
def calculate_user_skill_score(skills_with_score):
|
||||
"""
|
||||
Calculates user skill score to see where the user falls in a certain job category.
|
||||
"""
|
||||
# generate a dict with skill name as key and score as value
|
||||
# take only those skills that user has learned.
|
||||
|
||||
if not skills_with_score:
|
||||
return 0.0
|
||||
|
||||
skills_score_dict = {
|
||||
item['name']: item['score']
|
||||
for item in skills_with_score
|
||||
if item['score'] is not None
|
||||
}
|
||||
sum_of_skills = sum(skills_score_dict.values())
|
||||
skills_count = len(skills_score_dict)
|
||||
if not skills_count:
|
||||
return 0.0
|
||||
# sum of skills score in the category/ 3*no. of skills in category
|
||||
return round(sum_of_skills / (3 * skills_count), 1)
|
||||
|
||||
|
||||
def get_skills_score(skills, learner_skill_score):
|
||||
"""
|
||||
Takes each skill item in list and appends its score to it.
|
||||
For a skill that doesn't exist in learner's skills set, appends None as score.
|
||||
"""
|
||||
for skill in skills:
|
||||
skill['score'] = learner_skill_score.get(skill['name'])
|
||||
|
||||
|
||||
def update_category_user_scores_map(categories, category_user_scores_map):
|
||||
"""
|
||||
Appends user's scores for each category in the dict.
|
||||
"""
|
||||
for category in categories:
|
||||
category_user_scores_map[category['name']].append(category['user_score'])
|
||||
|
||||
|
||||
def update_edx_average_score(categories, user_score_mapping):
|
||||
"""
|
||||
Calculates average score for each category and appends it.
|
||||
"""
|
||||
for category in categories:
|
||||
category_scores = user_score_mapping[category['name']]
|
||||
sum_score = sum(category_scores, 0.0)
|
||||
average_score = round(sum_score / len(category_scores), 1)
|
||||
category['edx_average_score'] = average_score
|
||||
|
||||
|
||||
def get_base_url(url):
|
||||
"""
|
||||
Returns the base url for any given url.
|
||||
"""
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
return f'{parsed.scheme}://{parsed.netloc}'
|
||||
|
||||
|
||||
def get_top_skill_categories_for_job(job_id):
|
||||
"""
|
||||
Retrieve top categories for the job with the given job_id.
|
||||
|
||||
Arguments:
|
||||
job_id (int): id of the job about which we are retrieving information.
|
||||
|
||||
Returns:
|
||||
dict with top 5 categories of specified job.
|
||||
"""
|
||||
user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Skill Categories')
|
||||
if user:
|
||||
api_client = get_catalog_api_client(user)
|
||||
root_url = get_catalog_api_base_url()
|
||||
base_api_url = get_base_url(root_url)
|
||||
resource = '/taxonomy/api/v1/job-top-subcategories'
|
||||
cache_key = f'{catalog_integration.CACHE_KEY}.job-categories.{job_id}'
|
||||
data = get_api_data(
|
||||
catalog_integration,
|
||||
resource=resource,
|
||||
resource_id=job_id,
|
||||
api_client=api_client,
|
||||
base_api_url=base_api_url,
|
||||
cache_key=cache_key if catalog_integration.is_cache_enabled else None,
|
||||
)
|
||||
if data:
|
||||
return data
|
||||
|
||||
|
||||
def get_job_holder_usernames(job_id):
|
||||
"""
|
||||
Retrieve usernames of users who have the same job as given job_id.
|
||||
|
||||
Arguments:
|
||||
job_id (int): id of the job for which we are retrieving usernames.
|
||||
|
||||
Returns:
|
||||
list with oldest 100 users' usernames that exist in our system.
|
||||
"""
|
||||
user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Job Holder Usernames')
|
||||
if user:
|
||||
api_client = get_catalog_api_client(user)
|
||||
root_url = get_catalog_api_base_url()
|
||||
base_api_url = get_base_url(root_url)
|
||||
resource = '/taxonomy/api/v1/job-holder-usernames'
|
||||
cache_key = f'{catalog_integration.CACHE_KEY}.job-holder-usernames.{job_id}'
|
||||
data = get_api_data(
|
||||
catalog_integration,
|
||||
resource=resource,
|
||||
resource_id=job_id,
|
||||
api_client=api_client,
|
||||
base_api_url=base_api_url,
|
||||
cache_key=cache_key if catalog_integration.is_cache_enabled else None,
|
||||
)
|
||||
if data:
|
||||
return data
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
Views for learner_skill_levels.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
|
||||
from .api import get_learner_skill_levels
|
||||
from .utils import get_top_skill_categories_for_job, get_job_holder_usernames, update_category_user_scores_map, \
|
||||
update_edx_average_score
|
||||
|
||||
|
||||
class LearnerSkillLevelsView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Returns top 5 job categories for the given job. Checks which skill the user has learned via courses
|
||||
and assign scores to each skill in category. Also takes first 100 users in our system to calculate
|
||||
average score for each category.
|
||||
|
||||
**Request format**
|
||||
|
||||
GET /api/user/v1/skill_level/{job_id}/
|
||||
|
||||
**Response Values for GET**
|
||||
|
||||
If the specified job_id doesn't exist, an HTTP
|
||||
404 "Not Found" response is returned.
|
||||
|
||||
If a logged in user makes a request with an existing job, an HTTP 200
|
||||
"OK" response is returned that contains a JSON string.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/user/v1/skill_level/1/
|
||||
|
||||
**Example Response**
|
||||
|
||||
{
|
||||
"job": "Digital Product Manager",
|
||||
"skill_categories": [
|
||||
{
|
||||
"name": "Information Technology",
|
||||
"id": 1,
|
||||
"skills": [
|
||||
{"id": 2, "name": "Query Languages", "score": 1},
|
||||
{"id": 3, "name": "MongoDB", "score": 3},
|
||||
]
|
||||
"user_score": 0.4, // request user's score
|
||||
"edx_average_score": 0.7,
|
||||
"skills_subcategories": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Databases",
|
||||
"skills": [
|
||||
{"id": 2, "name": "Query Languages", "score": 1},
|
||||
{"id": 3, "name": "MongoDB", "score": None},
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "IT Management",
|
||||
"skills": [
|
||||
{"id": 1, "name": "Technology Roadmap", "score": 2},
|
||||
]
|
||||
},
|
||||
// here remaining job related skills subcategories
|
||||
]
|
||||
},
|
||||
|
||||
// Here more 4 skill categories
|
||||
]
|
||||
}
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
BearerAuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser
|
||||
)
|
||||
permission_classes = (permissions.IsAuthenticated, )
|
||||
|
||||
def get(self, request, job_id):
|
||||
"""
|
||||
GET /api/user/v1/skill_level/{job_id}/
|
||||
"""
|
||||
# get top categories for the given job
|
||||
job_skill_categories = get_top_skill_categories_for_job(job_id)
|
||||
if not job_skill_categories:
|
||||
return Response(
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
data={'message': "The job id doesn't exist, enter a valid job id."}
|
||||
)
|
||||
|
||||
# assign scores for every skill request user has learned
|
||||
top_categories = deepcopy(job_skill_categories['skill_categories'])
|
||||
user_category_scores = get_learner_skill_levels(
|
||||
user=request.user,
|
||||
top_categories=top_categories,
|
||||
)
|
||||
|
||||
# repeat the same logic for 100 job holder users in our system
|
||||
job_holder_usernames = get_job_holder_usernames(job_id)
|
||||
users = User.objects.filter(username__in=job_holder_usernames['usernames'])
|
||||
|
||||
# edx_avg_score should only be calculated if users count is greater than 5, else skip it.
|
||||
if len(users) > 5:
|
||||
# To save all the users' scores against every category to calculate average score
|
||||
category_user_scores_map = defaultdict(list)
|
||||
|
||||
for user in users:
|
||||
categories = deepcopy(job_skill_categories['skill_categories'])
|
||||
categories_with_scores = get_learner_skill_levels(
|
||||
user=user,
|
||||
top_categories=categories,
|
||||
)
|
||||
update_category_user_scores_map(categories_with_scores, category_user_scores_map)
|
||||
|
||||
update_edx_average_score(user_category_scores, category_user_scores_map)
|
||||
|
||||
job_skill_categories['skill_categories'] = user_category_scores
|
||||
return Response(job_skill_categories)
|
||||
@@ -19,7 +19,6 @@ from .accounts.views import (
|
||||
NameChangeView,
|
||||
UsernameReplacementView, CancelAccountRetirementStatusView
|
||||
)
|
||||
from .learner_skill_levels.views import LearnerSkillLevelsView
|
||||
from . import views as user_api_views
|
||||
from .models import UserPreference
|
||||
from .preferences.views import PreferencesDetailView, PreferencesView
|
||||
@@ -191,11 +190,6 @@ urlpatterns = [
|
||||
PreferencesDetailView.as_view(),
|
||||
name='preferences_detail_api'
|
||||
),
|
||||
re_path(
|
||||
r'^v1/skill_level/(?P<job_id>[0-9]+)/$',
|
||||
LearnerSkillLevelsView.as_view(),
|
||||
name="learner_skill_level"
|
||||
),
|
||||
# Moved from user_api/legacy_urls.py
|
||||
path('v1/', include(USER_API_ROUTER.urls)),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user