diff --git a/taxonomy/api/__init__.py b/taxonomy/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taxonomy/api/v1/__init__.py b/taxonomy/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taxonomy/api/v1/serializers.py b/taxonomy/api/v1/serializers.py new file mode 100644 index 00000000..6df21562 --- /dev/null +++ b/taxonomy/api/v1/serializers.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from taxonomy.models import Skill + + +class SkillSerializer(serializers.ModelSerializer): + """ Skill Searlizer """ + + class Meta: + model = Skill + fields = ('name', 'description') diff --git a/taxonomy/api/v1/urls.py b/taxonomy/api/v1/urls.py new file mode 100644 index 00000000..e219e098 --- /dev/null +++ b/taxonomy/api/v1/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from taxonomy.api.v1.views import SkillsView + +urlpatterns = [ + path('api/v1/skills/', SkillsView.as_view(), name='skill_list') +] diff --git a/taxonomy/api/v1/views.py b/taxonomy/api/v1/views.py new file mode 100644 index 00000000..e48e7578 --- /dev/null +++ b/taxonomy/api/v1/views.py @@ -0,0 +1,19 @@ +from rest_framework.generics import ListAPIView +from rest_framework.permissions import IsAuthenticated +from taxonomy.models import Skill +from taxonomy.api.v1.serializers import SkillSerializer + + +class SkillsView(ListAPIView): + """ List view for Skills """ + permission_classes = [IsAuthenticated] + serializer_class = SkillSerializer + + def get_queryset(self): + search = self.request.query_params.get('search') + + queryset = Skill.objects.exclude(courseskills__is_blacklisted=True) + if search: + queryset = queryset.filter(name__icontains=search) + + return queryset diff --git a/taxonomy/migrations/0010_auto_20210305_0914.py b/taxonomy/migrations/0010_auto_20210305_0914.py new file mode 100644 index 00000000..f5359aea --- /dev/null +++ b/taxonomy/migrations/0010_auto_20210305_0914.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-03-05 09:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taxonomy', '0009_skill_description'), + ] + + operations = [ + migrations.AlterField( + model_name='skill', + name='name', + field=models.CharField(blank=True, db_index=True, help_text='The name of the skill.', max_length=255), + ), + ] diff --git a/taxonomy/models.py b/taxonomy/models.py index 72d42e01..4bbbd824 100644 --- a/taxonomy/models.py +++ b/taxonomy/models.py @@ -31,6 +31,7 @@ class Skill(TimeStampedModel): name = models.CharField( max_length=255, blank=True, + db_index=True, help_text=_( 'The name of the skill.' ) diff --git a/taxonomy/urls.py b/taxonomy/urls.py index a9a39e96..06d13f6a 100644 --- a/taxonomy/urls.py +++ b/taxonomy/urls.py @@ -3,12 +3,16 @@ Taxonomy Connector URL Configuration. """ -from django.urls import re_path +from django.urls import re_path, include from taxonomy import views +from taxonomy.api.v1.urls import urlpatterns as api_v1_urlpatterns + urlpatterns = [ re_path( r"^admin/taxonomy/refresh_course_skills/$", views.RefreshCourseSkills.as_view(), name="refresh_course_skills" ), ] + +urlpatterns += api_v1_urlpatterns diff --git a/test_settings.py b/test_settings.py index ee3c8e20..1ea564df 100644 --- a/test_settings.py +++ b/test_settings.py @@ -35,6 +35,18 @@ def root(*args): 'django.contrib.sessions', 'django.contrib.messages', 'taxonomy', + 'rest_framework' +) + +MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.contrib.sites.middleware.CurrentSiteMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) LOCALE_PATHS = [ @@ -69,3 +81,9 @@ def root(*args): CELERY_BROKER_URL = 'memory://localhost/' ### END CELERY + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + ), +} diff --git a/test_utils/factories.py b/test_utils/factories.py index 9c75e7f9..e7a83e3c 100644 --- a/test_utils/factories.py +++ b/test_utils/factories.py @@ -5,6 +5,8 @@ import factory from faker import Factory as FakerFactory +from django.contrib.auth.models import User + from taxonomy.models import CourseSkills, Job, JobPostings, JobSkills, Skill FAKER = FakerFactory.create() @@ -94,3 +96,20 @@ class Meta: median_posting_duration = factory.LazyAttribute(lambda x: FAKER.pyint(min_value=0, max_value=100000000)) unique_postings = factory.LazyAttribute(lambda x: FAKER.pyint(min_value=0, max_value=100000000)) unique_companies = factory.LazyAttribute(lambda x: FAKER.pyint(min_value=0, max_value=100000000)) + + +class UserFactory(factory.django.DjangoModelFactory): + """ + Factory for User model. + """ + + class Meta: + model = User + + email = factory.LazyAttribute(lambda x: FAKER.email()) + username = factory.LazyAttribute(lambda x: FAKER.user_name()) + first_name = factory.LazyAttribute(lambda x: FAKER.first_name()) + last_name = factory.LazyAttribute(lambda x: FAKER.last_name()) + is_superuser = False + is_staff = False + is_active = True diff --git a/tests/test_api_v1.py b/tests/test_api_v1.py new file mode 100644 index 00000000..5ec94dd1 --- /dev/null +++ b/tests/test_api_v1.py @@ -0,0 +1,75 @@ +from pytest import mark +from django.urls import reverse + +from rest_framework.test import APITestCase +from rest_framework import status +from test_utils import factories + + +TEST_USERNAME = 'taxonomy_user' +TEST_EMAIL = 'taxonomy_user@example.com' +TEST_PASSWORD = 'password' + + +@mark.django_db +class TestSkillsView(APITestCase): + def setUp(self): + """ + Perform operations common to all tests. + """ + super().setUp() + self.user = self.create_user(username=TEST_USERNAME, email=TEST_EMAIL, password=TEST_PASSWORD) + + self.url = reverse('skill_list') + + self.whitelisted_skills = [ + 'C Plus Plus', 'Command Line Interface', 'Data Structures', 'Biochemistry', + 'Animations', 'Algorithms', 'Data Science', 'Data Wrangling', 'Databases' + ] + self.blacklisted_skills = ['Visual Basic', 'Oracle'] + + for skill_name in self.whitelisted_skills: + skill = factories.SkillFactory(name=skill_name) + factories.CourseSkillsFactory(skill=skill) + + for skill_name in self.blacklisted_skills: + skill = factories.SkillFactory(name=skill_name) + factories.CourseSkillsFactory(skill=skill, is_blacklisted=True) + + self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD) + + def create_user(self, username=TEST_USERNAME, password=TEST_PASSWORD, **kwargs): + """ + Create a test user and set its password. + """ + user = factories.UserFactory(username=username, **kwargs) + user.set_password(password) # pylint: disable=no-member + user.save() # pylint: disable=no-member + return user + + def test_search(self): + """ + Verify that skills endppoint return all skills when `search` query param is not given + """ + response = self.client.get(path=self.url) + assert response.status_code == status.HTTP_200_OK + skill_names = [skill['name'] for skill in response.json()] + assert sorted(skill_names) == sorted(self.whitelisted_skills) + + def test_search_with_query_param(self): + """ + Verify that skills endppoint return filtered skills according to the `search` query param + """ + response = self.client.get(path=self.url + '?search=data') + assert response.status_code == status.HTTP_200_OK + skill_names = [skill['name'] for skill in response.json()] + assert skill_names == ['Data Structures', 'Data Science', 'Data Wrangling', 'Databases' ] + + def test_search_with_blacklisted_skill(self): + """ + Verify that skills endppoint does not return blacklised skills. + """ + response = self.client.get(path=self.url + '?search=Oracle') + assert response.status_code == status.HTTP_200_OK + skill_names = [skill['name'] for skill in response.json()] + assert skill_names == []