From 7a3f55202e4e3b8f4315ca344b6ee4e522d65434 Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Sun, 6 Oct 2019 11:11:51 +0700 Subject: [PATCH 1/7] Create test for search --- core/tests/test_vacancies.py | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py index 1667ac1..277a787 100644 --- a/core/tests/test_vacancies.py +++ b/core/tests/test_vacancies.py @@ -41,6 +41,37 @@ class ApplicationTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) + @requests_mock.Mocker() + def test_application_search(self, m): + m.get('https://akun.cs.ui.ac.id/oauth/token/verify/?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', json={"username": 'dummy.mahasiswa', "role": 'mahasiswa', "identity_number": '1234567890'}, status_code=200) + m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ + "username": "dummy.mahasiswa", + "nama": "Dummy Mahasiswa", + "state": 1, + "kode_org": "01.00.12.01:mahasiswa", + "kodeidentitas": "1234567890", + "nama_role": "mahasiswa" + }, status_code=200) + m.get('https://api.cs.ui.ac.id/siakngcs/mahasiswa/1234567890?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', json={ + "kota_lahir": "kota_kota", + "tgl_lahir": "2017-12-31", + "program": [{ + "nm_org": "Ilmu Informasi", + "angkatan": "2017" + }] + }, status_code=200) + + url = '/api/login/' + + response = self.client.post(url, {'username': 'dummy.mahasiswa', 'password': 'lalala', 'login-type': 'sso-ui'}, + format='json') + student_id = response.data.get('student').get('id') + + url = '/api/students/' + str(student_id) + '/applied-vacancies/?search=engineer' + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + @requests_mock.Mocker() def test_application_create_and_delete(self, m): m.get('https://akun.cs.ui.ac.id/oauth/token/verify/?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', json={"username": 'dummy.mahasiswa', "role": 'mahasiswa', "identity_number": '1234567890'}, status_code=200) @@ -115,6 +146,42 @@ class BookmarkApplicationTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) + @requests_mock.Mocker() + def test_search_bookmarked_application(self, m): + m.get('https://akun.cs.ui.ac.id/oauth/token/verify/?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', + json={"username": 'dummy.mahasiswa', "role": 'mahasiswa', "identity_number": '1234567890'}, + status_code=200) + m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ + "username": "dummy.mahasiswa", + "nama": "Dummy Mahasiswa", + "state": 1, + "kode_org": "01.00.12.01:mahasiswa", + "kodeidentitas": "1234567890", + "nama_role": "mahasiswa" + }, status_code=200) + m.get( + 'https://api.cs.ui.ac.id/siakngcs/mahasiswa/1234567890?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', + json={ + "kota_lahir": "kota_kota", + "tgl_lahir": "2017-12-31", + "program": [{ + "nm_org": "Ilmu Informasi", + "angkatan": "2017" + }] + }, status_code=200) + + url = '/api/login/' + + response = self.client.post(url, + {'username': 'dummy.mahasiswa', 'password': 'lalala', 'login-type': 'sso-ui'}, + format='json') + student_id = response.data.get('student').get('id') + + url = '/api/students/' + str(student_id) + '/bookmarked-vacancies/?search=engineer' + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + @requests_mock.Mocker() def test_application_create_and_delete(self, m): m.get('https://akun.cs.ui.ac.id/oauth/token/verify/?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', json={"username": 'dummy.mahasiswa', "role": 'mahasiswa', "identity_number": '1234567890'}, status_code=200) @@ -165,6 +232,44 @@ class VacancyTest(APITestCase): response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + @requests_mock.Mocker() + def test_search_vacancy(self, m): + superuser = User.objects.create_user('dummy.company', 'dummy.company@company.com', 'lalala123') + company = Company.objects.create(user=superuser, description="This is a test company") + Vacancy.objects.create(company=company, open_time=datetime.now(), close_time=datetime.now(), name="Software Engineer") + Vacancy.objects.create(company=company, open_time=datetime.now(), close_time=datetime.now(), name="Data Engineer") + m.get('https://akun.cs.ui.ac.id/oauth/token/verify/?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', + json={"username": 'dummy.mahasiswa', "role": 'mahasiswa', "identity_number": '1234567890'}, + status_code=200) + m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ + "username": "dummy.mahasiswa", + "nama": "Dummy Mahasiswa", + "state": 1, + "kode_org": "01.00.12.01:mahasiswa", + "kodeidentitas": "1234567890", + "nama_role": "mahasiswa" + }, status_code=200) + m.get( + 'https://api.cs.ui.ac.id/siakngcs/mahasiswa/1234567890?client_id=X3zNkFmepkdA47ASNMDZRX3Z9gqSU1Lwywu5WepG', + json={ + "kota_lahir": "kota_kota", + "tgl_lahir": "2017-12-31", + "program": [{ + "nm_org": "Ilmu Informasi", + "angkatan": "2017" + }] + }, status_code=200) + + url = '/api/login/' + + self.client.post(url, {'username': 'dummy.mahasiswa', 'password': 'lalala', 'login-type': 'sso-ui'}, format='json') + + url = '/api/vacancies/?search=software' + response = self.client.get(url, format='json') + self.assertEqual(1, Company.objects.all().count()) + self.assertEqual(2, Vacancy.objects.all().count()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_unverified_vacancy_list(self): superuser = User.objects.create_superuser('dummy.company', 'dummy.company@company.com', 'lalala123') self.client.force_authenticate(user=superuser) -- GitLab From 4c7f5810ef982d7bc182a0c0b8f124ceca6026a6 Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Sun, 6 Oct 2019 11:40:28 +0700 Subject: [PATCH 2/7] Implement backend for search vacancy, application, and bookmark --- core/views/vacancies.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/core/views/vacancies.py b/core/views/vacancies.py index e4b8b98..49f3ebb 100644 --- a/core/views/vacancies.py +++ b/core/views/vacancies.py @@ -1,5 +1,6 @@ import requests from django.conf import settings +from django.db.models import Q from rest_framework import viewsets, status from rest_framework.decorators import detail_route from rest_framework.exceptions import ValidationError @@ -33,8 +34,12 @@ class VacancyViewSet(MultiSerializerViewSetMixin, viewsets.ModelViewSet): return super(VacancyViewSet, self).get_permissions() def list(self, request, *args, **kwargs): - vacancies = Vacancy.objects.all() verified = request.query_params['verified'] if 'verified' in request.query_params else "True" + search = request.query_params['search'] if 'search' in request.query_params else None + if search is not None: + vacancies = Vacancy.objects.filter(Q(name__icontains=search) | Q(company__user__username__icontains=search)) + else: + vacancies = Vacancy.objects.all() if verified.lower() in ("yes", "true", "t", "1"): vacancies = vacancies.filter(verified=True) if verified.lower() in {"no", "false", "f", "0"}: @@ -42,6 +47,7 @@ class VacancyViewSet(MultiSerializerViewSetMixin, viewsets.ModelViewSet): page = self.paginate_queryset(vacancies) if page is not None: return self.get_paginated_response(VacancySerializer(page, many=True, context={'request': request}).data) + vacancies = Vacancy.objects.all() return Response(VacancySerializer(vacancies, many=True, context={'request': request}).data) @detail_route(permission_classes=[IsAdminOrCompany]) @@ -118,7 +124,11 @@ class StudentApplicationViewSet(viewsets.GenericViewSet): """ student = get_object_or_404(Student.objects.all(), pk=student_id) vacancy_ids = Application.objects.filter(student=student).values('vacancy') - vacancies = Vacancy.objects.filter(id__in=vacancy_ids) + search = request.query_params['search'] if 'search' in request.query_params else None + if search is not None: + vacancies = Vacancy.objects.filter(Q(id__in=vacancy_ids) & (Q(name__icontains=search) | Q(company__user__username__icontains=search))) + else: + vacancies = Vacancy.objects.filter(id__in=vacancy_ids) page = self.paginate_queryset(vacancies) if page is not None: return self.get_paginated_response(VacancySerializer(page, many=True, context={'request': request}).data) @@ -242,7 +252,11 @@ class BookmarkedVacancyByStudentViewSet(viewsets.GenericViewSet): --- """ student = get_object_or_404(Student.objects.all(), pk=student_id) - vacancies = student.bookmarked_vacancies.all() + search = request.query_params['search'] if 'search' in request.query_params else None + if search is not None: + vacancies = student.bookmarked_vacancies.filter(Q(name__icontains=search) | Q(company__user__username__icontains=search)) + else: + vacancies = student.bookmarked_vacancies.all() page = self.paginate_queryset(vacancies) if page is not None: return self.get_paginated_response(VacancySerializer(page, many=True, context={'request': request}).data) -- GitLab From 6c1f9d571a0aab39cd25e0c969787d489a7139a2 Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Sun, 6 Oct 2019 13:24:49 +0700 Subject: [PATCH 3/7] Integrate backend with frontend --- assets/css/custom.css | 23 ++++++++ assets/js/components/Pagination.jsx | 90 +++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/assets/css/custom.css b/assets/css/custom.css index 9b7cf50..5cba8ec 100755 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -289,4 +289,27 @@ card .formRegis{ .ui.segment.kop { line-height: 5px; +} + +.search-container { + width: fit-content; + margin: auto; +} + +.search-form { + border: none; + border-bottom: solid #0d5aa7 2px; + text-decoration: none; + outline: none; + font-size: 14pt; +} + +.search-button { + background-color: #0d5aa7; + color: white; + border: none; + padding: 10px; + font-size: 14pt; + border-radius: 5px; + margin-left: 10px; } \ No newline at end of file diff --git a/assets/js/components/Pagination.jsx b/assets/js/components/Pagination.jsx index d2a50ae..d76dbcd 100644 --- a/assets/js/components/Pagination.jsx +++ b/assets/js/components/Pagination.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Menu, Container, Icon, Loader } from 'semantic-ui-react'; import Server from '../lib/Server'; import ModalAlert from '../components/ModalAlert'; +import Pane from "./Pane"; export default class Pagination extends React.Component { @@ -29,38 +30,85 @@ export default class Pagination extends React.Component { dir: 0, start: true, finish: false, + search: '', }; this.handleNext = this.handleNext.bind(this); this.handlePrev = this.handlePrev.bind(this); this.getItemsData = this.getItemsData.bind(this); this.handleMovement = this.handleMovement.bind(this); + this.handleSearchChange = this.handleSearchChange.bind(this); + this.handleSearchSubmit = this.handleSearchSubmit.bind(this); this.refresh = this.refresh.bind(this); this.content = this.content.bind(this); this.pageMenu = this.pageMenu.bind(this); this.getItemsData(); } - getItemsData = () => Server.get(this.state.url, false).then((data) => { - this.setState({ current: this.state.current + this.state.dir }); - this.setState( - { items: data.results, - next: `${this.props.url}?page=${this.state.current + 1}`, - prev: `${this.props.url}?page=${this.state.current - 1}`, - loading: false, + getItemsData = () => { + const prefix = this.state.dir === 0 ? '?' : '&'; + const searchPrefix = this.state.search !== '' ? prefix : ''; + const searchValue = this.state.search !== '' ? `search=${this.state.search}` : ''; + Server.get(`${this.state.url}${searchPrefix}${searchValue}`, false).then((data) => { + console.log('GET ITEM DATA'); + this.setState({ + current: this.state.current + this.state.dir, + }); + this.setState( + { + items: data.results, + next: `${this.props.url}?page=${this.state.current + 1}`, + prev: `${this.props.url}?page=${this.state.current - 1}`, + loading: false, + }); + let first = true; + let last = true; + if (data.previous) { + first = false; + } + if (data.next) { + last = false; + } + this.setState({first, last}); + }, error => error.then((r) => { + this.modalAlert.open(this.props.error, r.detail); + this.setState({loading: false}); + })); + }; + + handleSearchChange(e) { + this.setState({ search: e.target.value }); + if (e.target.value === '') { + this.setState({ + items: [], + current: 1, + next: '', + prev: '', + url: this.props.url, + loading: true, + dir: 0, + start: true, + finish: false, + }, function() { + this.getItemsData(); }); - let first = true; - let last = true; - if (data.previous) { - first = false; - } - if (data.next) { - last = false; } - this.setState({ first, last }); - }, error => error.then((r) => { - this.modalAlert.open(this.props.error, r.detail); - this.setState({ loading: false }); - })); + } + + handleSearchSubmit() { + this.setState({ + items: [], + current: 1, + next: '', + prev: '', + url: this.props.url, + loading: true, + dir: 0, + start: true, + finish: false, + }, function() { + this.getItemsData(); + }); + } refresh() { this.forceUpdate(); @@ -112,6 +160,10 @@ export default class Pagination extends React.Component { render = () => (
+
+ + +
{ this.modalAlert = modal; }} /> {!this.state.loading && this.content()} {!this.state.loading && this.pageMenu()} -- GitLab From 0d4b5ca893b847c9aa859c06a7ac3009954e4449 Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Mon, 7 Oct 2019 19:00:46 +0700 Subject: [PATCH 4/7] Fix migration conflict --- core/migrations/0021_auto_20191007_0648.py | 2 +- core/migrations/0025_merge_20191007_1810.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 core/migrations/0025_merge_20191007_1810.py diff --git a/core/migrations/0021_auto_20191007_0648.py b/core/migrations/0021_auto_20191007_0648.py index 612ada1..743bfd3 100644 --- a/core/migrations/0021_auto_20191007_0648.py +++ b/core/migrations/0021_auto_20191007_0648.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( + migrations.AlterField( model_name='company', name='website', field=models.CharField(default=b'Belum ada link website', max_length=100), diff --git a/core/migrations/0025_merge_20191007_1810.py b/core/migrations/0025_merge_20191007_1810.py new file mode 100644 index 0000000..5619c26 --- /dev/null +++ b/core/migrations/0025_merge_20191007_1810.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-10-07 11:10 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0021_auto_20191007_0648'), + ('core', '0024_auto_20191007_1533'), + ] + + operations = [ + ] -- GitLab From 5c897e8ee8dc874f02679c34a0fb1c4cebaa19fd Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Tue, 8 Oct 2019 05:27:17 +0700 Subject: [PATCH 5/7] Resolve with updated master --- core/migrations/0026_merge_20191008_0525.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 core/migrations/0026_merge_20191008_0525.py diff --git a/core/migrations/0026_merge_20191008_0525.py b/core/migrations/0026_merge_20191008_0525.py new file mode 100644 index 0000000..31a0321 --- /dev/null +++ b/core/migrations/0026_merge_20191008_0525.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-10-07 22:25 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0025_merge_20191007_1810'), + ('core', '0025_merge_20191007_2124'), + ('core', '0025_merge_20191008_0048'), + ] + + operations = [ + ] -- GitLab From 5e5918aa22e9b4f6d28e67a9387ca004af28a6dc Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Tue, 8 Oct 2019 06:48:34 +0700 Subject: [PATCH 6/7] return async function --- assets/js/components/Pagination.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/components/Pagination.jsx b/assets/js/components/Pagination.jsx index d76dbcd..d278cb5 100644 --- a/assets/js/components/Pagination.jsx +++ b/assets/js/components/Pagination.jsx @@ -48,7 +48,7 @@ export default class Pagination extends React.Component { const prefix = this.state.dir === 0 ? '?' : '&'; const searchPrefix = this.state.search !== '' ? prefix : ''; const searchValue = this.state.search !== '' ? `search=${this.state.search}` : ''; - Server.get(`${this.state.url}${searchPrefix}${searchValue}`, false).then((data) => { + return Server.get(`${this.state.url}${searchPrefix}${searchValue}`, false).then((data) => { console.log('GET ITEM DATA'); this.setState({ current: this.state.current + this.state.dir, -- GitLab From 8a0eea0858e34d9416d80ccf49b14e95c929417c Mon Sep 17 00:00:00 2001 From: Ilham Darmawan Date: Tue, 8 Oct 2019 06:52:50 +0700 Subject: [PATCH 7/7] Pull from master and merge migration --- core/migrations/0027_merge_20191008_0652.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 core/migrations/0027_merge_20191008_0652.py diff --git a/core/migrations/0027_merge_20191008_0652.py b/core/migrations/0027_merge_20191008_0652.py new file mode 100644 index 0000000..cd5f54d --- /dev/null +++ b/core/migrations/0027_merge_20191008_0652.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-10-07 23:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0026_merge_20191008_0525'), + ('core', '0026_merge_20191008_0256'), + ] + + operations = [ + ] -- GitLab