diff --git a/.DS_Store b/.DS_Store deleted file mode 100755 index 877d8f79f51a6e99addb143fe20b35e312133f9a..0000000000000000000000000000000000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/assets/js/components/ApproveModal.jsx b/assets/js/components/ApproveModal.jsx index 2f609b3618176232ba17cb1f0236a7a8ed350c19..a08313c94a10da01472b95f9188e59e92a676600 100755 --- a/assets/js/components/ApproveModal.jsx +++ b/assets/js/components/ApproveModal.jsx @@ -17,6 +17,7 @@ export default class ApproveModal extends React.Component { modalOpen: false, rejectLoading: false, acceptLoading: false, + formValue: '', finishLoading: false, }; this.handleOpen = this.handleOpen.bind(this); @@ -26,6 +27,7 @@ export default class ApproveModal extends React.Component { this.gotoStudentProfile = this.gotoStudentProfile.bind(this); this.gotoStudentResume = this.gotoStudentResume.bind(this); this.gotoStudentTranscript = this.gotoStudentTranscript.bind(this); + this.formValueHandler = this.formValueHandler.bind(this); } componentWillUpdate() { @@ -47,6 +49,10 @@ export default class ApproveModal extends React.Component { this.setState({ modalOpen: false }); }; + formValueHandler(event) { + this.setState({ formValue: event }); + } + readApplication = () => { const data = { status: Applicant.APPLICATION_STATUS.READ }; return ( @@ -58,7 +64,7 @@ export default class ApproveModal extends React.Component { }; rejectApplication = () => { - const data = { status: Applicant.APPLICATION_STATUS.REJECTED }; + const data = { status: Applicant.APPLICATION_STATUS.REJECTED, reason: this.state.formValue }; this.setState({ rejectLoading: true }); Server.patch(`/applications/${this.props.data.id}/`, data).then((status) => { this.props.updateStatus(this.props.data.id, status.status); @@ -71,6 +77,7 @@ export default class ApproveModal extends React.Component { 'Apakah anda yakin untuk menolak lamaran ini?', 'trash', this.rejectApplication, + true, ); }; @@ -107,6 +114,7 @@ export default class ApproveModal extends React.Component { 'Apakah anda yakin untuk menerima lamaran ini?', 'checkmark', this.acceptApplication, + false, ); }; @@ -131,9 +139,7 @@ export default class ApproveModal extends React.Component { onClose={this.handleClose} > <ConfirmationModal - ref={(modal) => { - this.modal = modal; - }} + ref={(modal) => { this.modal = modal; }} formValueHandler={this.formValueHandler} /> <Modal.Header>Data Lamaran</Modal.Header> <Modal.Content> diff --git a/assets/js/components/ConfirmationModal.jsx b/assets/js/components/ConfirmationModal.jsx index 800e30175e6fd8b4cf1858ff6c6f3a7cc97e2767..75357a654c9dcf0f64a006b049a6677ad35ec04f 100755 --- a/assets/js/components/ConfirmationModal.jsx +++ b/assets/js/components/ConfirmationModal.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Modal, Button, Icon, Header } from 'semantic-ui-react'; +import { Modal, Button, Icon, Header, Form } from 'semantic-ui-react'; export default class ConfirmationModal extends React.Component { @@ -10,11 +10,15 @@ export default class ConfirmationModal extends React.Component { modalOpen: false, header: '', content: '', + formValue: '', + formPlaceholder: 'Tulis alasan penolakan', icon: 'trash', + isForm: false, callback: () => {}, }; this.open = this.open.bind(this); this.handleYes = this.handleYes.bind(this); + this.handleChange = this.handleChange.bind(this); } componentWillUpdate() { @@ -34,10 +38,17 @@ export default class ConfirmationModal extends React.Component { modalOpen: false, }); + handleChange = (event) => { + this.setState({ formValue: event.target.value }); + this.props.formValueHandler(this.state.formValue); + }; + handleYes = () => { this.state.callback(); this.handleClose(); } - open = (header = this.state.header, content = this.state.content, icon = this.state.icon, callback = this.state.callback()) => { - this.setState({ modalOpen: true, header, content, callback, icon }); + open = ( + header = this.state.header, content = this.state.content, icon = this.state.icon, + callback = this.state.callback(), isForm = this.state.isForm) => { + this.setState({ modalOpen: true, header, content, callback, icon, isForm }); }; render = () => ( @@ -48,6 +59,8 @@ export default class ConfirmationModal extends React.Component { <Header icon={this.state.icon} content={this.state.header} /> <Modal.Content> <p>{this.state.content}</p> + {this.state.isForm ? + <Form.Input style={{ width: '650px', height: '100px' }} type="text" placeholder={this.state.formPlaceholder} value={this.state.value} onChange={this.handleChange} /> : null } </Modal.Content> <Modal.Actions> <Button onClick={this.handleClose} basic color="red" inverted> diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 0476aef680399eb3bf4e51b26ec096ff5b1ea1da..5ab7aa3833b5a0a231977cb2ec19688914a78e6a 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.17 on 2019-10-12 08:21 +# Generated by Django 1.11.17 on 2019-10-12 09:54 from __future__ import unicode_literals import core.lib.validators @@ -76,6 +76,7 @@ class Migration(migrations.Migration): ('photo', models.FileField(blank=True, null=True, upload_to=core.models.accounts.get_student_photo_file_path, validators=[core.lib.validators.validate_image_file_extension])), ('portfolio_link', models.URLField(blank=True, null=True)), ('linkedin_url', models.URLField(blank=True, null=True)), + ('hackerrank_url', models.URLField(blank=True, null=True)), ('website_url', models.URLField(blank=True, null=True)), ('work_experience', models.CharField(blank=True, max_length=500, null=True)), ('region', models.CharField(blank=True, max_length=30, null=True)), @@ -140,6 +141,13 @@ class Migration(migrations.Migration): ('vacancy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='milestones', to='core.Vacancy')), ], ), + migrations.CreateModel( + name='ReasonRejected', + fields=[ + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='core.Application')), + ('reason', models.TextField(default=b'Tidak memenuhi kualifikasi perusahaan.')), + ], + ), migrations.AddField( model_name='student', name='applied_vacancies', diff --git a/core/models/vacancies.py b/core/models/vacancies.py index 219c4d8f540c3ef7b47097d68e24567d3babbccd..6ad88fe5a8492bd56a3e6d98b97881992114c407 100755 --- a/core/models/vacancies.py +++ b/core/models/vacancies.py @@ -22,7 +22,7 @@ class Vacancy(models.Model): @property def apply_before(self): if(self.close_time<timezone.now()): - return "Pendaftaran ditutup" + return "Pendaftaran ditutup" return "Daftar sebelum "+self.close_time.strftime('%d')+" "+self.close_time.strftime('%B')+" "+self.close_time.strftime('%Y') class Meta: @@ -47,6 +47,11 @@ class Application(models.Model): unique_together = (("student", "vacancy"),) +class ReasonRejected(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, primary_key=True) + reason = models.TextField(default='Tidak memenuhi kualifikasi perusahaan.') + + class VacancyMilestone(models.Model): vacancy = models.ForeignKey(Vacancy, on_delete=models.CASCADE, related_name="milestones", null=False) name = models.CharField(max_length=100, null=False) diff --git a/core/serializers/vacancies.py b/core/serializers/vacancies.py index aca8891b551b7159a351d29ffedccdcc59cec4cf..911afdf774e40b01c7d5a429fbc2994293886787 100755 --- a/core/serializers/vacancies.py +++ b/core/serializers/vacancies.py @@ -1,7 +1,7 @@ from rest_framework import serializers from core.models import Company -from core.models.vacancies import Vacancy, Application, VacancyMilestone +from core.models.vacancies import Vacancy, Application, VacancyMilestone, ReasonRejected from core.serializers.accounts import StudentSerializer, CompanySerializer @@ -65,6 +65,13 @@ class ApplicationStatusSerializer(serializers.ModelSerializer): model = Application fields = ['status'] +class ReasonRejectedSerializer(serializers.ModelSerializer): + application = ApplicationSerializer() + + class Meta: + model = ReasonRejected + fields = '__all__' + class SupervisorStudentApplicationSerializer(serializers.ModelSerializer): def to_representation(self, instance): status_map = ["new", "read", "bookmarked", "rejected", "accepted","aborted" ] diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py index 119521ee20a9755b6b049e576fec140af88c9991..6050ff6adc9a6f14412e29509d6e96654cb8c47c 100755 --- a/core/tests/test_vacancies.py +++ b/core/tests/test_vacancies.py @@ -14,7 +14,9 @@ from core.serializers.vacancies import VacancySerializer class ApplicationTests(APITestCase): @requests_mock.Mocker() def test_application_list(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.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", @@ -23,14 +25,16 @@ class ApplicationTests(APITestCase): "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) + 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/' @@ -76,7 +80,9 @@ class ApplicationTests(APITestCase): @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) + 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", @@ -85,14 +91,16 @@ class ApplicationTests(APITestCase): "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) + 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/' @@ -197,7 +205,9 @@ class ApplicationTests(APITestCase): class BookmarkApplicationTests(APITestCase): @requests_mock.Mocker() def test_application_list(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.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", @@ -206,14 +216,16 @@ class BookmarkApplicationTests(APITestCase): "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) + 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/' @@ -264,7 +276,9 @@ class BookmarkApplicationTests(APITestCase): @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) + 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", @@ -273,14 +287,16 @@ class BookmarkApplicationTests(APITestCase): "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) + 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/' @@ -675,7 +691,8 @@ class CompanyListsTests(APITestCase): def test_company_application_list_with_major(self): new_user = User.objects.create_user('dummy.company4', 'dummy.company4@company.com', 'lalala123') - new_company = Company.objects.create(user=new_user, description="lalala", status=Company.VERIFIED, logo=None, address=None) + new_company = Company.objects.create(user=new_user, description="lalala", status=Company.VERIFIED, logo=None, + address=None) self.client.force_authenticate(new_user) @@ -683,6 +700,7 @@ class CompanyListsTests(APITestCase): response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + class SupervisorStudentApplicationTests(APITestCase): def test_list_student_application(self): new_user = User.objects.create_user('dummy.supervisor', 'dummy.supervisor@asd.asd', 'lalala123') @@ -983,7 +1001,7 @@ class AcceptOneOfferTests(APITestCase): new_user2 = User.objects.create_user('dummy.company2', 'dummy.company2@company.com', 'lalala123') new_company2 = Company.objects.create(user=new_user2, description="lalala", status=Company.VERIFIED, logo=None, - address=None) + address=None) new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0), description="lalala", requirements= "requirements", close_time=datetime.today()) @@ -1021,7 +1039,7 @@ class AcceptOneOfferTests(APITestCase): self.assertTrue('aborted' in status_response) def test_student_not_exist_given_auth(self): - new_user3,new_vacancy, new_vacancy2, new_student = self.generateObject() + new_user3, new_vacancy, new_vacancy2, new_student = self.generateObject() self.client.force_authenticate(new_user3) diff --git a/core/views/vacancies.py b/core/views/vacancies.py index 0ee98338b9bf53ad3b75a4dadb96bc173f37ea86..29382c6bd02625956d28cba86ab463c9c2ce8737 100755 --- a/core/views/vacancies.py +++ b/core/views/vacancies.py @@ -16,7 +16,7 @@ from core.lib.mixins import MultiSerializerViewSetMixin from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVacancyOwner, AsAdminOrSupervisor, \ VacancyApprovalPermission, IsAdminOrVacancyOwnerOrAuthenticatedReadOnly from core.models import Student, Company -from core.models.vacancies import Vacancy, Application, VacancyMilestone +from core.models.vacancies import Vacancy, Application, VacancyMilestone, ReasonRejected from core.serializers.vacancies import VacancySerializer, ApplicationSerializer, ApplicationStatusSerializer, \ PostVacancySerializer, VacancyVerifiedSerializer, SupervisorStudentApplicationSerializer, \ VacancyMilestoneSerializer @@ -186,6 +186,8 @@ class ApplicationViewSet(MultiSerializerViewSetMixin, viewsets.GenericViewSet): serializer = self.get_serializer_class()(application, data=request.data, partial=True) if serializer.is_valid(): serializer.save() + if request.data['status'] == 3: + ReasonRejected.objects.create(application=application, reason=request.data['reason']) return Response(serializer.data, status=status.HTTP_202_ACCEPTED) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -261,12 +263,15 @@ class StudentApplicationViewSet(viewsets.GenericViewSet): application.delete() return Response(ApplicationSerializer(application, context={'request': request}).data) + class StatusError(Exception): pass + class UnauthorizeError(Exception): pass + class CompanyApplicationViewSet(viewsets.GenericViewSet): queryset = Application.objects.all() permission_classes = [IsAdminOrCompany] @@ -295,7 +300,7 @@ class CompanyApplicationViewSet(viewsets.GenericViewSet): return Response({"error": "forbidden"}, status=status.HTTP_403_FORBIDDEN) except (StatusError, ValueError): return Response({"error": "status must be an integer between 0 and 4"}, \ - status=status.HTTP_400_BAD_REQUEST) + status=status.HTTP_400_BAD_REQUEST) @detail_route(methods=["get"]) def by_vacancy(self, request, company_id, pk=None): @@ -323,7 +328,7 @@ class CompanyApplicationViewSet(viewsets.GenericViewSet): return Response({"error": "forbidden"}, status=status.HTTP_403_FORBIDDEN) except (StatusError, ValueError): return Response({"error": "status must be an integer between 0 and 4"}, \ - status=status.HTTP_400_BAD_REQUEST) + status=status.HTTP_400_BAD_REQUEST) def __get_company_list_by_company_id(self, request, company_id): company = get_object_or_404(Company.objects.all().order_by('-updated'), pk=company_id) @@ -335,7 +340,7 @@ class CompanyApplicationViewSet(viewsets.GenericViewSet): return request.user.is_superuser or request.user == company.user def __get_vacancy_list_by_pk(self, pk, company): - vacancy = get_object_or_404(Vacancy.objects.all(), pk=pk) + vacancy = get_object_or_404(Vacancy.objects.all(), pk=pk) if not self.__validating_vacancy(vacancy, company): raise UnauthorizeError return vacancy @@ -361,6 +366,7 @@ class CompanyApplicationViewSet(viewsets.GenericViewSet): raise StatusError return True + class CompanyVacanciesViewSet(viewsets.GenericViewSet): queryset = Vacancy.objects.all() pagination_class = PageNumberPagination diff --git a/kape/files/student-ui-ux-portofolio/a78eefaf-9e2f-402a-965d-67b9aa9e6ea6.pdf b/kape/files/student-ui-ux-portofolio/a78eefaf-9e2f-402a-965d-67b9aa9e6ea6.pdf new file mode 100644 index 0000000000000000000000000000000000000000..379cb93fd41385e7ea21e9f02d44b2fb84fd860d Binary files /dev/null and b/kape/files/student-ui-ux-portofolio/a78eefaf-9e2f-402a-965d-67b9aa9e6ea6.pdf differ diff --git a/kape/files/student-ui-ux-portofolio/d62c211d-c5f4-4ab9-afbe-58df4af67789.pdf b/kape/files/student-ui-ux-portofolio/d62c211d-c5f4-4ab9-afbe-58df4af67789.pdf new file mode 100644 index 0000000000000000000000000000000000000000..379cb93fd41385e7ea21e9f02d44b2fb84fd860d Binary files /dev/null and b/kape/files/student-ui-ux-portofolio/d62c211d-c5f4-4ab9-afbe-58df4af67789.pdf differ