Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support in skills quiz api to accept skill names #106

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Change Log

Unreleased

[1.22.2] - 2022-09-06
---------------------
* Added support in Skills Quiz API to accept name of skills instead of primary keys.

[1.22.1] - 2022-08-26
---------------------
* Added id field in JobSerializer for Algolia.
Expand Down
2 changes: 1 addition & 1 deletion taxonomy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
# 2. MINOR version when you add functionality in a backwards compatible manner, and
# 3. PATCH version when you make backwards compatible bug fixes.
# More details can be found at https://semver.org/
__version__ = '1.22.1'
__version__ = '1.22.2'

default_app_config = 'taxonomy.apps.TaxonomyConfig' # pylint: disable=invalid-name
10 changes: 9 additions & 1 deletion taxonomy/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
from django.contrib import admin

from taxonomy.models import (
CourseSkills, Job, JobPostings, JobSkills, Skill, Translation, SkillCategory, SkillSubCategory, SkillsQuiz
CourseSkills,
Job,
JobPostings,
JobSkills,
Skill,
SkillCategory,
SkillsQuiz,
SkillSubCategory,
Translation,
)


Expand Down
23 changes: 23 additions & 0 deletions taxonomy/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Taxonomy API serializers.
"""
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer

from taxonomy.models import CourseSkills, Job, JobPostings, JobSkills, Skill, SkillsQuiz
Expand Down Expand Up @@ -66,3 +67,25 @@ class Meta:
model = SkillsQuiz
fields = '__all__'
read_only_fields = ('username', )


class SkillsQuizBySkillNameSerializer(ModelSerializer):
skill_names = serializers.ListField(child=serializers.CharField(), required=False)

class Meta:
model = SkillsQuiz
fields = '__all__'
read_only_fields = ('username', 'skills')

def validate(self, attrs):
attrs = super().validate(attrs)
skill_names = attrs.pop('skill_names')
attrs['skills'] = Skill.get_skill_ids_by_name(skill_names)
return attrs

def validate_skill_names(self, skill_names):
valid_skill_names = Skill.objects.filter(name__in=skill_names).values_list('name', flat=True)
if len(valid_skill_names) < len(skill_names):
invalid_skill_names = list(set(skill_names) - set(valid_skill_names))
raise serializers.ValidationError(f"Invalid skill names: {invalid_skill_names}")
return skill_names
19 changes: 14 additions & 5 deletions taxonomy/api/v1/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
"""
Taxonomy API views.
"""
from rest_framework import permissions
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import permissions, status
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet

from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Prefetch

from taxonomy.api.permissions import IsOwner
from taxonomy.api.v1.serializers import (
JobPostingsSerializer, JobsListSerializer, SkillListSerializer, SkillsQuizSerializer
JobPostingsSerializer,
JobsListSerializer,
SkillListSerializer,
SkillsQuizSerializer,
SkillsQuizBySkillNameSerializer,
)
from taxonomy.models import CourseSkills, Job, JobPostings, Skill, SkillsQuiz
from taxonomy.api.permissions import IsOwner


class TaxonomyAPIViewSetMixin:
Expand Down Expand Up @@ -76,7 +81,6 @@ class SkillsQuizViewSet(TaxonomyAPIViewSetMixin, ModelViewSet):
"""
ViewSet to list and retrieve all JobPostings in the system.
"""
serializer_class = SkillsQuizSerializer
permission_classes = (permissions.IsAuthenticated, IsOwner | permissions.IsAdminUser, )
filter_backends = (DjangoFilterBackend,)
filterset_fields = ('username', )
Expand All @@ -99,3 +103,8 @@ def get_queryset(self):
queryset = queryset.filter(username=self.request.user.username)

return queryset.all().select_related('current_job').prefetch_related('skills', 'future_jobs')

def get_serializer_class(self):
if self.action == 'create':
return SkillsQuizBySkillNameSerializer
return SkillsQuizSerializer
9 changes: 9 additions & 0 deletions taxonomy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from __future__ import unicode_literals

import uuid
from typing import List

from solo.models import SingletonModel

from django.db import models
from django.utils.translation import gettext_lazy as _

from model_utils.models import TimeStampedModel

from taxonomy.choices import UserGoal


Expand Down Expand Up @@ -93,6 +95,13 @@ def __repr__(self):
"""
return '<Skill id="{}" name="{}">'.format(self.id, self.name)

@classmethod
def get_skill_ids_by_name(cls, skill_names: List[str]) -> List[int]:
"""
Return all the matching skill IDs from given skill names.
"""
return list(cls.objects.filter(name__in=skill_names).values_list('id', flat=True))

class Meta:
"""
Meta configuration for Skill model.
Expand Down
8 changes: 4 additions & 4 deletions taxonomy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
"""
import logging
import time
import boto3

import boto3
from bs4 import BeautifulSoup
from edx_django_utils.cache import get_cache_key, TieredCache
from edx_django_utils.cache import TieredCache, get_cache_key

from taxonomy.constants import (
AMAZON_TRANSLATION_ALLOWED_SIZE,
AUTO,
EMSI_API_RATE_LIMIT_PER_SEC,
ENGLISH,
REGION,
TRANSLATE_SERVICE,
EMSI_API_RATE_LIMIT_PER_SEC
)
from taxonomy.emsi.client import EMSISkillsApiClient
from taxonomy.exceptions import TaxonomyAPIError
from taxonomy.models import CourseSkills, ProgramSkill, JobSkills, Skill, Translation
from taxonomy.models import CourseSkills, JobSkills, ProgramSkill, Skill, Translation
from taxonomy.serializers import SkillSerializer

LOGGER = logging.getLogger(__name__)
Expand Down
8 changes: 7 additions & 1 deletion test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ def root(*args):
root('taxonomy', 'conf', 'locale'),
]

# ROOT_URLCONF = 'taxonomy.urls'
ROOT_URLCONF = 'taxonomy.urls'

SECRET_KEY = 'insecure-secret-key'

MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware',
)

# Settings related to to EMSI client
# API URLs are altered to avoid accidentally calling the API in tests
Expand Down
12 changes: 11 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.test import TestCase

from taxonomy.models import Job, JobPostings
from taxonomy.models import Job, JobPostings, Skill
from test_utils import factories


Expand Down Expand Up @@ -68,6 +68,16 @@ def test_string_representation(self):
assert expected_str == skill.__str__()
assert expected_repr == skill.__repr__()

def test_get_skill_ids_by_name(self):
"""
Test the ``get_skill_ids_by_name`` Return correct IDs.
"""
skill_a = factories.SkillFactory()
skill_b = factories.SkillFactory()
skill_c = factories.SkillFactory() # pylint: disable=unused-variable
skill_ids = Skill.get_skill_ids_by_name([skill_a.name, skill_b.name])
assert skill_ids == [skill_a.id, skill_b.id]


@mark.django_db
class TestCourseSkills(TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import mock
from pytest import mark

from taxonomy.models import CourseSkills, Skill, ProgramSkill
from taxonomy.models import CourseSkills, ProgramSkill, Skill
from taxonomy.signals.signals import UPDATE_COURSE_SKILLS, UPDATE_PROGRAM_SKILLS
from test_utils.mocks import MockCourse, MockProgram
from test_utils.providers import DiscoveryCourseMetadataProvider, DiscoveryProgramMetadataProvider
Expand Down
2 changes: 1 addition & 1 deletion tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pytest import mark
from testfixtures import LogCapture

from taxonomy.models import CourseSkills, Skill, ProgramSkill
from taxonomy.models import CourseSkills, ProgramSkill, Skill
from taxonomy.tasks import update_course_skills, update_program_skills
from test_utils.mocks import MockCourse, MockProgram
from test_utils.providers import DiscoveryCourseMetadataProvider, DiscoveryProgramMetadataProvider
Expand Down
74 changes: 74 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
Tests for the taxonomy API views.
"""

import json

from pytest import mark

from django.contrib.auth import get_user_model
from django.test import Client, TestCase

from test_utils.factories import JobFactory, SkillFactory

User = get_user_model() # pylint: disable=invalid-name
USER_PASSWORD = 'QWERTY'


@mark.django_db
class TestSkillsQuizViewSet(TestCase):
"""
Tests for ``SkillsQuizViewSet`` view set.
"""

def setUp(self) -> None:
super(TestSkillsQuizViewSet, self).setUp()
self.skill_a = SkillFactory()
self.skill_b = SkillFactory()
self.job_a = JobFactory()
self.job_b = JobFactory()
self.user = User.objects.create(username="rocky")
self.user.set_password(USER_PASSWORD)
self.user.save()
self.client = Client()
self.client.login(username=self.user.username, password=USER_PASSWORD)
self.view_url = r'/api/v1/skills-quiz/'

def test_skills_quiz_api_post(self):
"""
Test the Post endpoint of API.
"""
post_data = {
'goal': 'change_careers',
'current_job': self.job_a.id,
'skill_names': [self.skill_a.name, self.skill_b.name],
'future_jobs': [self.job_a.id, self.job_b.id]
}
response = self.client.post(self.view_url, json.dumps(post_data), 'application/json')
assert response.status_code == 201
response = response.json()
assert response['goal'] == post_data['goal']
assert response['current_job'] == post_data['current_job']
assert response['username'] == self.user.username
assert response['skills']
assert response['skills'] == [self.skill_a.id, self.skill_b.id]
assert response['future_jobs'] == post_data['future_jobs']
assert 'skill_names' not in response

def test_validation_error_for_skill_names(self):
"""
Test the validation error if wrong skill is sent in the post data.
"""
unsaved_skill = 'skill not saved'
post_data = {
'goal': 'change_careers',
'current_job': self.job_a.id,
'skill_names': [self.skill_a.name, unsaved_skill],
'future_jobs': [self.job_a.id, self.job_b.id]
}
response = self.client.post(self.view_url, json.dumps(post_data), 'application/json')
assert response.status_code == 400
response = response.json()
assert 'skill_names' in response
assert response['skill_names'] == [f"Invalid skill names: ['{unsaved_skill}']"]