diff --git a/assets/css/custom.css b/assets/css/custom.css index 9b7cf50a1356d41530baa447984f2fb2abfd873a..5cba8ecf80180ee7ea93f79b3754f18e9afd0141 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 d2a50ae2229e941d73ec3c5eda0a4c534f620ded..d278cb504272e2aaa86c59b7f2c7dec67d3a2de0 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}` : ''; + 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, + }); + 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 = () => ( <div> <Loader active={this.state.loading} /> + <div className="search-container"> + <input type="text" placeholder="Cari Lowongan" onChange={this.handleSearchChange} className="search-form"/> + <button type="submit" onClick={this.handleSearchSubmit} className="search-button">Cari</button> + </div> <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> {!this.state.loading && this.content()} {!this.state.loading && this.pageMenu()} diff --git a/core/migrations/0020_merge_20191007_0648.py b/core/migrations/0020_merge_20191007_0648.py new file mode 100644 index 0000000000000000000000000000000000000000..3a42b13e8acad531a1666be45051b43fca0318e4 --- /dev/null +++ b/core/migrations/0020_merge_20191007_0648.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-10-06 23:48 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0017_vacancy_amount'), + ('core', '0019_merge_20191006_0852'), + ('core', '0014_auto_20191004_1340'), + ] + + operations = [ + ] diff --git a/core/migrations/0021_auto_20191007_0648.py b/core/migrations/0021_auto_20191007_0648.py new file mode 100644 index 0000000000000000000000000000000000000000..743bfd3cfe7b551934a307926873f3ceb9e76ad5 --- /dev/null +++ b/core/migrations/0021_auto_20191007_0648.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-10-06 23:48 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_merge_20191007_0648'), + ] + + operations = [ + migrations.AlterField( + model_name='company', + name='website', + field=models.CharField(default=b'Belum ada link website', max_length=100), + ), + migrations.AlterField( + model_name='student', + name='phone_number', + field=models.CharField(blank=True, db_index=True, max_length=12, null=True, validators=[django.core.validators.RegexValidator(b'^0\\d{1,11}$')]), + ), + ] diff --git a/core/migrations/0025_merge_20191007_1810.py b/core/migrations/0025_merge_20191007_1810.py new file mode 100644 index 0000000000000000000000000000000000000000..5619c2624abb04e45e085a015e8d8bc95eb795ac --- /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 = [ + ] diff --git a/core/migrations/0026_merge_20191008_0525.py b/core/migrations/0026_merge_20191008_0525.py new file mode 100644 index 0000000000000000000000000000000000000000..31a03215fe2031121fa5683008856ba25469124d --- /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 = [ + ] diff --git a/core/migrations/0027_merge_20191008_0652.py b/core/migrations/0027_merge_20191008_0652.py new file mode 100644 index 0000000000000000000000000000000000000000..cd5f54dc9175743548528801af4f27a066c7ca5e --- /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 = [ + ] diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py index 4e37048ec406da450390a5dc6b450c133cdca3aa..668444fb09ffb8a9c37dddffec020869791b0c8b 100644 --- a/core/tests/test_vacancies.py +++ b/core/tests/test_vacancies.py @@ -43,6 +43,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) @@ -152,6 +183,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) @@ -202,6 +269,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) diff --git a/core/views/vacancies.py b/core/views/vacancies.py index e4d0d6674e3973398c3f63e7a6e5400cf7b3f494..46f79ab776bfc6a38f6aac09a77cc070d8e8c23a 100644 --- a/core/views/vacancies.py +++ b/core/views/vacancies.py @@ -1,6 +1,7 @@ import requests from django.utils import timezone from django.conf import settings +from django.db.models import Q from rest_framework import viewsets, status from rest_framework.decorators import detail_route, permission_classes from rest_framework.exceptions import ValidationError @@ -37,8 +38,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() companies = [int(x) for x in request.query_params.getlist('company', [])] if verified.lower() in ("yes", "true", "t", "1"): vacancies = vacancies.filter(verified=True) @@ -57,6 +62,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) def name_position_validator(self, names): @@ -185,7 +191,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) @@ -355,7 +365,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)