diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6dc284f51091b8db9e5fc6a3c6eeb043c31e246b..d1e008c30cfc7fd5c06d894fed1f237c57abc7d9 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,8 @@ test: - export CHROME_BIN=/usr/bin/google-chrome - curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - - sudo apt-get install -y nodejs + - sudo apt-get install -y build-essential + - npm install npm -g - npm install - npm run build-production - service postgresql start diff --git a/assets/css/custom.css b/assets/css/custom.css index 2fcdeb99f6f2557150a71d4420e78b32cbb1000e..5dc7c2c44302b86d1bc8e18b7f43a9dda1e92584 100755 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -275,6 +275,7 @@ border-color: transparent; .ui.segment.biodata-section b{ color: white; } + .admin-bar{ margin: 0; padding: 0 10px 0 0; @@ -282,4 +283,5 @@ border-color: transparent; text-align: right; background-color: #304D8A; color: white; -} \ No newline at end of file +} + diff --git a/assets/js/CompanyPage.jsx b/assets/js/CompanyPage.jsx index c21db4e93f9b3d8002886ececc3cd605a7314c4a..f84648e8a271500e97b6051dcfff4aa1cdeaf1a0 100644 --- a/assets/js/CompanyPage.jsx +++ b/assets/js/CompanyPage.jsx @@ -12,6 +12,7 @@ export default class CompanyPage extends React.Component { }; handleClick = () => window.open('/admin/'); + handleVacancy = () => window.open('/lowongan'); render() { return ( @@ -19,6 +20,11 @@ export default class CompanyPage extends React.Component { <div style={{ paddingLeft: '10px' }}> <Button onClick={this.handleClick} icon="dashboard" labelPosition="left" color="facebook" content="Buka Menu Administrasi" /> </div> + + <div style={{ paddingLeft: '10px', paddingTop : '10px'}}> + <Button onClick={this.handleVacancy} icon="dashboard" labelPosition="left" color="facebook" content="Halaman Verifikasi Lowongan" /> + </div> + <Tabs selected={0}> <Pagination key={1} diff --git a/assets/js/CreateVacancy.jsx b/assets/js/CreateVacancy.jsx index 7fbf853fbc6abfee5999bfb9c9896dbbbb8c377f..e521df67a8b9bb00bb89871e5c2a4c43dd2eafc5 100644 --- a/assets/js/CreateVacancy.jsx +++ b/assets/js/CreateVacancy.jsx @@ -3,9 +3,9 @@ import { Segment, Button, Form, Header, Icon, Input } from 'semantic-ui-react'; import { browserHistory } from 'react-router'; import DatePicker from 'react-datepicker'; import moment from 'moment'; +import CKEditor from 'react-ckeditor-wrapper'; import ModalAlert from './components/ModalAlert'; import Server from './lib/Server'; -import Dumper from './lib/Dumper'; export default class CreateVacancy extends React.Component { @@ -19,11 +19,14 @@ export default class CreateVacancy extends React.Component { /* istanbul ignore next */ this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.handleEditorChange = this.handleEditorChange.bind(this); + this.handleEditor = this.handleEditor.bind(this); this.setCloseTime = this.setCloseTime.bind(this); this.setOpenTime = this.setOpenTime.bind(this); this.state = { formLoading: false, + loading: !!this.props.params.id, company: this.props.user.data.company, vacancyId: this.props.params.id, open_time: moment(), @@ -32,14 +35,17 @@ export default class CreateVacancy extends React.Component { description: '', }; - this.state.vacancyId && Server.get(`/vacancies/${this.state.vacancyId}/`).then((r) => { - this.setState({ - description: r.description, - name: r.name, - open_time: moment(r.open_time), - close_time: moment(r.close_time), + if (this.state.vacancyId) { + Server.get(`/vacancies/${this.state.vacancyId}/`).then((r) => { + this.setState({ + description: r.description, + name: r.name, + open_time: moment(r.open_time), + close_time: moment(r.close_time), + loading: false, + }); }); - }); + } } setOpenTime(date) { @@ -54,14 +60,29 @@ export default class CreateVacancy extends React.Component { this.setState({ [e.target.name]: e.target.value }); }; + handleEditor(value) { + this.setState({ description: value }); + console.log('dor'); + } + + handleEditorChange = (e) => { + this.setState({ description: e.target.getContent() }); + console.log('Content was updated:', this.state.description); + }; + handleSubmit = (e) => { e.preventDefault(); + console.log(this.state); this.setState({ formLoading: true }); - const data = Object.assign({}, this.state); - data.open_time = data.open_time.format(); - data.close_time = data.close_time.format(); - data.company = this.state.company.id; + const data = {}; + data.name = this.state.name; + data.description = this.state.description; + data.open_time = this.state.open_time.format(); + data.close_time = this.state.close_time.format(); + if (!this.state.vacancyId) { + data.company = this.state.company.id; + } const url = this.state.vacancyId ? `/vacancies/${this.state.vacancyId}/` : '/vacancies/'; const method = this.state.vacancyId ? 'PATCH' : 'POST'; @@ -74,6 +95,21 @@ export default class CreateVacancy extends React.Component { })); }; + modules = { + toolbar: [ + [{ header: [1, 2, false] }], + ['bold', 'italic', 'underline', 'strike', 'blockquote'], + [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], + ['link', 'image'], + ['clean'], + ], + }; + + formats = ['header', 'bold', 'italic', 'underline', 'strike', 'blockquote', + 'list', 'bullet', 'indent', + 'link', 'image', + ]; + render = () => ( <div className="create-lowongan" > <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> @@ -86,13 +122,23 @@ export default class CreateVacancy extends React.Component { </Header> <Form loading={this.state.formLoading} onSubmit={this.handleSubmit}> <Form.Field label="Posisi" name="name" control={Input} onChange={this.handleChange} value={this.state.name} required /> - <Form.TextArea - name="description" - label="Deskripsi" - placeholder="Deskripsi Lowongan..." - onChange={this.handleChange} - value={this.state.description} required - /> + { !this.state.loading && <CKEditor value={this.state.description} onChange={this.handleEditor} /> } + {/*<TinyMCE*/} + {/*content={this.state.description}*/} + {/*config={{*/} + {/*plugins: 'link image code',*/} + {/*toolbar: 'undo redo | bold italic | alignleft aligncenter alignright | code image',*/} + {/*}}*/} + {/*onChange={this.handleEditorChange}*/} + {/*/>*/} + {/*<Form.TextArea*/} + {/*name="description"*/} + {/*label="Deskripsi"*/} + {/*placeholder="Deskripsi Lowongan..."*/} + {/*onChange={this.handleChange}*/} + {/*value={this.state.description} required*/} + {/*/>*/} + <script>CKEDITOR.replace( 'description' );</script> <Form.Group widths="equal"> <Form.Field className="open-time-field" diff --git a/assets/js/Dashboard.jsx b/assets/js/Dashboard.jsx index 14b2e341afd98fbd65f61b5f6819c65b73467891..2b0c69dcc3218f3cf1cafb85faaf9b75e21f1d4a 100755 --- a/assets/js/Dashboard.jsx +++ b/assets/js/Dashboard.jsx @@ -1,5 +1,6 @@ import React from 'react'; import TopMenu from './components/TopMenu'; +import Server from './lib/Server'; import Footer from './components/Footer'; export default class Dashboard extends React.Component { @@ -13,6 +14,11 @@ export default class Dashboard extends React.Component { ]).isRequired, }; + constructor(props) { + super(props); + /* istanbul ignore next */ + } + render = () => ( <div> <TopMenu user={this.props.user} /> diff --git a/assets/js/ProfilePage.jsx b/assets/js/ProfilePage.jsx index 564e286731a39812dd1f0b8d2050087104cb147d..c56f441bce8da2e5843046e69c9348fb047b15d5 100644 --- a/assets/js/ProfilePage.jsx +++ b/assets/js/ProfilePage.jsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Segment, Image, Header, Icon, Checkbox, Container, Button, Form, Grid, Card } from 'semantic-ui-react'; +import { Segment, Image, Header, Icon, Checkbox, Container, Button, Form, Grid } from 'semantic-ui-react'; import Server from './lib/Server'; +import Storage from './lib/Storage'; import ModalAlert from './components/ModalAlert'; export default class ProfilePage extends React.Component { @@ -35,12 +36,15 @@ export default class ProfilePage extends React.Component { show_transcript: '', }, bagikanTranskrip: '', + acceptedNo: 0, + refresh: 1, }; this.getProfile = this.getProfile.bind(this); this.handleChange = this.handleChange.bind(this); this.handleCheckbox = this.handleCheckbox.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleFile = this.handleFile.bind(this); + this.gotoLink = this.gotoLink.bind(this); this.getProfile(); } @@ -60,10 +64,17 @@ export default class ProfilePage extends React.Component { phone_number: data.phone_number, photo: data.photo, show_transcript: data.show_transcript, - bagikanTranskrip: (data.show_transcript ? 'Ya' : 'Tidak'), + acceptedNo: data.accepted_no, + bagikanTranskrip: data.show_transcript, + refresh: this.state.refresh + 1, }); + if (this.props.route.own) { + const newSession = this.props.user.data; + newSession.student = data; + Storage.set('user-data', newSession); + window.scrollTo(0, 0); + } }, error => error.then(() => { - // this.modalAlert.open('Gagal Mengambil ', r.error); this.state.name = 'Gagal mendapatkan informasi'; })); } @@ -78,9 +89,9 @@ export default class ProfilePage extends React.Component { } }); Server.submit(`/profiles/students/${this.state.id}/`, submitForm, 'PATCH').then(() => { - this.modalAlert.open('Profil berhasil diperbaharui', 'Silakan periksa kembali profil anda'); + this.modalAlert.open('Profil berhasil diperbaharui', 'Silakan periksa kembali profil anda', this.getProfile); }, error => error.then((r) => { - this.modalAlert.open('Pembaharuan profil gagal', r.error); + this.modalAlert.open('Pembaharuan profil gagal', r.detail); })); }; @@ -103,6 +114,11 @@ export default class ProfilePage extends React.Component { this.setState({ form, show_transcript: d.checked }); }; + gotoLink = (url) => { + const win = window.open(url); + win.focus(); + }; + updateForm(show) { if (show) { return ( @@ -114,7 +130,7 @@ export default class ProfilePage extends React.Component { </Header.Content> </Header> <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> - <Form size="small" onSubmit={this.handleSubmit}> + <Form ref={(input) => { this.form = input; }} key={this.state.refresh} size="small" onSubmit={this.handleSubmit}> <Form.Field> <label htmlFor="photo">Profile Picture</label> <input onChange={this.handleFile} placeholder="Profile Photo.jpg" name="photo" type="File" /> @@ -164,77 +180,70 @@ export default class ProfilePage extends React.Component { <Grid.Column width={6}> <Container textAlign="left" className="profile-biodata"> <div className="biodata"> - - - <Segment basic textAlign="center"> - <h1> { this.state.name } </h1> - </Segment> - - <Segment basic vertical> - <Grid> - <Grid.Column width={2}> - <Icon name="university" size="big" /> - </Grid.Column> - <Grid.Column width={13}> - <h3> { this.state.major }, { this.state.batch } </h3> - </Grid.Column> - </Grid> - </Segment> - - <Segment basic vertical> - - <Grid> - <Grid.Column width={2}> - <Icon name="mail" size="big" /> - </Grid.Column> - <Grid.Column width={13}> - <h3> { this.state.email } </h3> - </Grid.Column> - </Grid> - </Segment> - - <Segment basic vertical> - <Grid> - <Grid.Column width={2}> - <Icon name="phone" size="big" /> - </Grid.Column> - <Grid.Column width={13}> - <h3> { this.state.phone_number }</h3> - </Grid.Column> - </Grid> - </Segment> - - <Segment basic vertical> - <Grid> - <Grid.Column width={2}> - <Icon name="gift" size="big" /> - </Grid.Column> - <Grid.Column width={13}> - <h3> { this.state.cityOfBirth}, { this.state.dateOfBirth } </h3> - </Grid.Column> - </Grid> - </Segment> - + <Segment basic textAlign="center"> + <h1> { this.state.name } </h1> + </Segment> + + <Segment basic vertical> + <Grid> + <Grid.Column width={2}> + <Icon name="university" size="big" /> + </Grid.Column> + <Grid.Column width={13}> + <h3> { this.state.major }, { this.state.batch } </h3> + </Grid.Column> + </Grid> + </Segment> + + <Segment basic vertical> + + <Grid> + <Grid.Column width={2}> + <Icon name="mail" size="big" /> + </Grid.Column> + <Grid.Column width={13}> + <h3> { this.state.email } </h3> + </Grid.Column> + </Grid> + </Segment> + + <Segment basic vertical> + <Grid> + <Grid.Column width={2}> + <Icon name="phone" size="big" /> + </Grid.Column> + <Grid.Column width={13}> + <h3> { this.state.phone_number }</h3> + </Grid.Column> + </Grid> + </Segment> + + <Segment basic vertical> + <Grid> + <Grid.Column width={2}> + <Icon name="gift" size="big" /> + </Grid.Column> + <Grid.Column width={13}> + <h3> { this.state.cityOfBirth}, { this.state.dateOfBirth } </h3> + </Grid.Column> + </Grid> + </Segment> </div> <Container textAlign="center"> - <div className="button-profile"> + <div className="button-profile"> <a href={this.state.resume || '#'} ><Button primary size="small">Resume</Button></a> </div> <div> <h4> Bagikan Transkrip : { this.state.bagikanTranskrip }</h4> </div> </Container> - </Container> </Grid.Column > - - </Grid> </Segment > { this.updateForm(this.props.route.own) } </div> - ); } } diff --git a/assets/js/VacancyPage.jsx b/assets/js/VacancyPage.jsx index 2032db0e1fbc003f2e3ff6e15ae2f732702ae77b..ac61b0a7d402ee783a73fae57b81785ed52b6ffb 100644 --- a/assets/js/VacancyPage.jsx +++ b/assets/js/VacancyPage.jsx @@ -1,9 +1,9 @@ import React from 'react'; +import { Container } from 'semantic-ui-react'; import Tabs from './components/Tabs'; import Pane from './components/Pane'; import VacancyList from './components/VacancyList'; import Pagination from './components/Pagination'; -import Applicants from './SupervisorPage'; export default class VacancyPage extends React.Component { @@ -15,8 +15,10 @@ export default class VacancyPage extends React.Component { const role = user.role; if (role === 'student') { return user.data.student.id; - } else if (role === 'company' || role === 'admin') { + } else if (role === 'company' || (role === 'admin' && user.data.company != null)) { return user.data.company.id; + } else if (role === 'supervisor' || (role === 'admin' && user.data.supervisor != null)) { + return user.data.supervisor.id; } return 0; @@ -31,6 +33,7 @@ export default class VacancyPage extends React.Component { }; } + generateVacancies() { if (this.props.user.role === 'student') { return ( @@ -48,7 +51,7 @@ export default class VacancyPage extends React.Component { } /> </Pane> - <Pane label="Lamaran saya" > + <Pane label="Lamaran Saya" > <Pagination key={2} url={`/students/${this.state.id}/applied-vacancies/`} @@ -76,10 +79,12 @@ export default class VacancyPage extends React.Component { </Pane> </Tabs> ); - } else if (this.props.user.role === 'company' || this.props.user.role === 'admin') { + } else if ((this.props.user.role === 'admin' && this.props.user.data.company != null) + || this.props.user.role === 'company') { return ( - <div> + <Container className="vacancies"> <Pagination + key={1} url={`/companies/${this.state.id}/vacancies/`} child={ <VacancyList @@ -87,17 +92,41 @@ export default class VacancyPage extends React.Component { user={this.props.user} userId={this.state.id} /> - } + } error="Anda belum diverifikasi. Harap hubungi admin" /> - - </div> + </Container> ); - } else if (this.props.user.role === 'admin') { + } else if (this.props.user.role === 'admin' || this.props.user.role === 'supervisor') { return ( - <div> - <h1> halo </h1> - </div> + <Tabs selected={0}> + <Pane label="Lowongan Belum Terverifikasi" > + <Pagination + key={1} + url="/vacancies/?verified=false" + child={ + <VacancyList + user={this.props.user} + key={1} + userId={this.state.id} + /> + } + /> + </Pane> + <Pane label="Lowongan Terverifikasi" > + <Pagination + key={2} + url="/vacancies/?verified=true" + child={ + <VacancyList + user={this.props.user} + key={2} + userId={this.state.id} + /> + } + /> + </Pane> + </Tabs> ); } diff --git a/assets/js/__test__/CompanyPage-test.jsx b/assets/js/__test__/CompanyPage-test.jsx index 04dfdcaa8c24bab3b7be2c0aa5f5bf13df59c004..1daa1ab8fd34bad0a70dd8377246164700c48756 100644 --- a/assets/js/__test__/CompanyPage-test.jsx +++ b/assets/js/__test__/CompanyPage-test.jsx @@ -43,7 +43,7 @@ describe('CompanyPage', () => { it('click dashboard button problem', () => { const companyPage = ReactTestUtils.renderIntoDocument( <CompanyPage user={adminUser} />); - const dashboardButton = ReactTestUtils.findRenderedDOMComponentWithTag(companyPage, 'Button'); + const dashboardButton = ReactTestUtils.scryRenderedDOMComponentsWithTag(companyPage, 'Button')[0]; ReactTestUtils.Simulate.click(dashboardButton); expect(companyPage).to.exist; expect(dashboardButton).to.exist; diff --git a/assets/js/__test__/components/AdminVacancy-test.jsx b/assets/js/__test__/components/AdminVacancy-test.jsx index 3f7d305c14aec7b30a62ac0383307c2d10dab188..3ed5a1b0f011ca14c10eddcbc532023bdaa8896a 100644 --- a/assets/js/__test__/components/AdminVacancy-test.jsx +++ b/assets/js/__test__/components/AdminVacancy-test.jsx @@ -1,12 +1,98 @@ +/* eslint-disable no-unused-expressions */ import React from 'react'; import ReactTestUtils from 'react-addons-test-utils'; import AdminVacancy from '../../components/AdminVacancy'; +import Storage from '../../lib/Storage'; describe('AdminVacancy', () => { - it('renders without problem', () => { - const adminVacancy = ReactTestUtils.renderIntoDocument( - <AdminVacancy />); - expect(adminVacancy).to.exist; + const fetchMock = require('fetch-mock'); + const response = { + close_time: '2019-03-28T05:55:42Z', + company: { + address: 'kebayoran baru', + id: 1, + logo: null, + name: 'tutup lapak', + }, + created: '2017-03-28T07:05:47.128672Z', + description: 'Lorem ipsum dolbh.', + id: 3, + name: 'Software Engineer', + open_time: '2017-03-28T05:55:38Z', + updated: '2017-03-28T07:34:13.122093Z', + verified: true, + }; + + const response2 = { + close_time: '2019-03-28T05:55:42Z', + company: { + address: 'kebayoran baru', + id: 1, + logo: 'pictures', + name: 'tutup lapak', + }, + created: '2017-03-28T07:05:47.128672Z', + description: 'Lorem ipsum dolbh.', + id: 3, + name: 'Software Engineer', + open_time: '2017-03-28T05:55:38Z', + updated: '2017-03-28T07:34:13.122093Z', + verified: false, + }; + + const supervisorUser = { + role: 'supervisor', + data: { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + student: { + id: 3, + user: { + url: 'http://localhost:8000/api/users/9/', + username: 'muhammad.reza42', + email: 'muhammad.reza42@ui.ac.id', + is_staff: false, + }, + name: 'Muhammad R.', + created: '2017-03-28T13:33:46.147241Z', + updated: '2017-03-28T13:33:46.148248Z', + npm: 1406543593, + resume: null, + phone_number: null, + birth_place: null, + birth_date: null, + major: null, + batch: null, + show_resume: false, + bookmarked_vacancies: [ + 3, + 2, + ], + applied_vacancies: [ + 3, + 1, + ], + }, + }, + }; + + it('renders for verified without problem', () => { + const lowongan = ReactTestUtils.renderIntoDocument( + <AdminVacancy status={4} data={response} />); + expect(lowongan).to.exist; }); -}); + it('renders for unverified without problem', () => { + const lowongan = ReactTestUtils.renderIntoDocument( + <AdminVacancy status={3} data={response2} />); + expect(lowongan).to.exist; + }); + + it('generate button without problem', () => { + const lowongan = ReactTestUtils.renderIntoDocument( + <AdminVacancy status={3} data={response2} />); + expect(lowongan.generateButton()).to.exist; + }); +}); diff --git a/assets/js/__test__/components/VacancyList-test.jsx b/assets/js/__test__/components/VacancyList-test.jsx index 37389b290aa4d7588d02034ad6bcf802a1d113c3..0ac2b46c3132983fe241dd3c7567ba7b3949b9e5 100644 --- a/assets/js/__test__/components/VacancyList-test.jsx +++ b/assets/js/__test__/components/VacancyList-test.jsx @@ -34,6 +34,46 @@ describe('VacancyList', () => { }, }; + const supervisorUser = { + role: 'supervisor', + data: { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + company: null, + supervisor: { + id: 3, + user: { + url: 'http://localhost:8000/api/users/9/', + username: 'muhammad.reza42', + email: 'muhammad.reza42@ui.ac.id', + is_staff: false, + }, + name: 'Muhammad R.', + created: '2017-03-28T13:33:46.147241Z', + updated: '2017-03-28T13:33:46.148248Z', + npm: 1406543593, + resume: null, + phone_number: null, + birth_place: null, + birth_date: null, + major: null, + batch: null, + show_resume: false, + bookmarked_vacancies: [ + 3, + 2, + ], + applied_vacancies: [ + 3, + 1, + ], + }, + student: null, + }, + }; + const studentUser = { role: 'student', data: { @@ -281,6 +321,20 @@ describe('VacancyList', () => { expect(vacancyList.generateVacancies()).to.exist; }); + it('renders without problem for supervisor', () => { + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList items={newResponse} userId={3} user={supervisorUser} />); + vacancyList.state.vacancies = newResponse; + expect(vacancyList.generateVacancies()).to.exist; + }); + + it('update status without problem', () => { + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList items={newResponse} userId={3} user={supervisorUser} />); + vacancyList.state.vacancies = newResponse; + vacancyList.updateStatus(4, 1); + }); + it('success delete vacancy', () => { fetchMock.restore(); fetchMock.delete('*', response2); @@ -299,6 +353,7 @@ describe('VacancyList', () => { it('fails delete vacancy', (done) => { fetchMock.restore(); fetchMock.delete('*', 404); + fetchMock.get('*', response2); const vacancyList = ReactTestUtils.renderIntoDocument( <VacancyList userId={1} items={newResponse} user={companyUser} deleteCallback={() => {}} />, ); diff --git a/assets/js/__test__/components/VerifyAdminModal-test.jsx b/assets/js/__test__/components/VerifyAdminModal-test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..84523ac190ce9df8fdfedfd23491284dd2e3804b --- /dev/null +++ b/assets/js/__test__/components/VerifyAdminModal-test.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import VerifyAdminModal from '../../components/VerifyAdminModal'; +import fetchMock from 'fetch-mock'; +import Storage from '../../lib/Storage'; + +describe('VerifyAdminModal', () => { + + it('renders without problem', () => { + const verifyModal = ReactTestUtils.renderIntoDocument( + <VerifyAdminModal />, + ); + expect(verifyModal).to.exist; + }); + + it('open without problem', () => { + const verifyModal = ReactTestUtils.renderIntoDocument( + <VerifyAdminModal id={4} />); + + const modal = ReactTestUtils.findRenderedDOMComponentWithTag(verifyModal, 'Button'); + ReactTestUtils.Simulate.click(modal); + expect(verifyModal.state.modalOpen).to.equal(true); + }); + + it('close without problem', () => { + const verifyModal = ReactTestUtils.renderIntoDocument( + <VerifyAdminModal id={4} />); + + verifyModal.handleClose(); + expect(verifyModal.state.modalOpen).to.equal(false); + }); +}); diff --git a/assets/js/components/AdminVacancy.jsx b/assets/js/components/AdminVacancy.jsx index 5a68ce36897b45d6e2bfa8768227b479dde018fa..3983a7cda5214da343393a2048edf5a1292ef428 100644 --- a/assets/js/components/AdminVacancy.jsx +++ b/assets/js/components/AdminVacancy.jsx @@ -1,23 +1,49 @@ import React from 'react'; -import { Button, Icon, Item, Grid } from 'semantic-ui-react'; -import AdminVerificationModal from './AdminVerificationModal'; +import { Item, Grid, Button } from 'semantic-ui-react'; +import VerifyAdminModal from './VerifyAdminModal'; +import Server from '../lib/Server'; -export default class AdminVacancyPage extends React.Component { +export default class AdminVacancy extends React.Component { + static propTypes = { + data: React.PropTypes.object.isRequired, + updateStatus: React.PropTypes.func.isRequired, + }; + + changeVerifiedStatus() { + let data = {}; + if (this.props.data.verified) { + data = { verified: false }; + } else { + data = { verified: true }; + } + Server.patch(`/vacancies/${this.props.data.id}/verify/`, data).then((status) => { + this.props.updateStatus(this.props.data.id, status.status); + }); + } + + generateButton() { + const unverifyButton = <Button floated="right" color="red" onClick={this.changeVerifiedStatus.bind(this)}>Batalkan Verifikasi</Button>; + const verifyButton = <Button floated="right" color="blue" onClick={this.changeVerifiedStatus.bind(this)}>Verifikasi</Button>; + + if (this.props.data.verified) { + return unverifyButton; + } + return verifyButton; + } render() { return ( <Item className="adminItems"> <Item.Image src="http://semantic-ui.com/images/wireframe/image.png" size="small" /> <Item.Content> - <Item.Header as="a">Software Engineer</Item.Header> + <Item.Header as="a">{this.props.data.name}</Item.Header> <Grid.Row> <Grid.Column floated="left"> - <h4>Jalanloka </h4> - Jakarta Barat, DKI Jakarta + <h4>{this.props.data.company.name} </h4> + {this.props.data.company.address} </Grid.Column> <Grid.Column floated="right"> - <h4> <Icon name="remove circle" size="large" color="red" /> Belum Terverifikasi </h4> - <AdminVerificationModal /> + {this.generateButton()} </Grid.Column> </Grid.Row> </Item.Content> diff --git a/assets/js/components/ApplyModal.jsx b/assets/js/components/ApplyModal.jsx index 6a830a8664e2dfbd252cfd0848491783a940b5d1..0fb1af1126d9caa8317b2aec2e7c8848821c21b2 100644 --- a/assets/js/components/ApplyModal.jsx +++ b/assets/js/components/ApplyModal.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon, Modal, Button, TextArea, Form } from 'semantic-ui-react'; +import { Icon, Modal, Button, TextArea, Form, Message } from 'semantic-ui-react'; import ModalAlert from './../components/ModalAlert'; import Server from './../lib/Server'; @@ -49,43 +49,51 @@ export default class ApplyModal extends React.Component { ); }; - render = () => ( - <Modal - trigger={<Button primary onClick={this.handleOpen} floated="right">{this.props.buttonTitle}</Button>} - closeIcon="close" - open={this.state.modalOpen} - onClose={this.handleClose} - > - <Modal.Header>{this.props.data.header}</Modal.Header> - <Modal.Content> - <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> - <Modal.Description> - <Modal.Header> <h3> Deskripsi Lowongan </h3></Modal.Header> - {this.props.data.description} - - </Modal.Description> - {this.props.active && ( - <div className="coverLetter"> - <br /> - <div className="linkCV"> - <a href={this.props.resume} target="_blank" rel="noopener noreferrer"> Klik untuk lihat CV terakhirmu</a> - </div> - <br /> - <div> - <h5>Cover Letter </h5> - <Form > - <TextArea placeholder="Tell us more" size="big" onChange={this.handleChange} /> - </Form> + render() { + return ( + <Modal + trigger={<Button primary onClick={this.handleOpen} floated="right">{this.props.buttonTitle}</Button>} + closeIcon="close" + open={this.state.modalOpen} + onClose={this.handleClose} + > + <Modal.Header>{this.props.data.header}</Modal.Header> + <Modal.Content> + <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> + <Modal.Description> + <Modal.Header> <h3> Deskripsi Lowongan </h3></Modal.Header> + { <div dangerouslySetInnerHTML={{ __html: this.props.data.description }} /> } + </Modal.Description> + {this.props.active && ( + <div className="coverLetter"> + <br /> + <div className="linkCV"> + { this.props.resume ? (<a href={this.props.resume} target="_blank" rel="noopener noreferrer"> Klik untuk lihat CV terakhirmu</a>) + : ( + <Message + error + icon="warning sign" + header="CV Tidak Ditemukan" + content="Anda belum mengunggah CV. Harap ubah profil anda terlebih dahulu pada halaman Profil." + />) + } + </div> + <br /> + <div> + <h5>Cover Letter </h5> + <Form > + <TextArea placeholder="Tell us more" size="big" onChange={this.handleChange} /> + </Form> + </div> </div> - </div> - )} - - </Modal.Content> - <Modal.Actions> - <Button loading={this.state.load} color="blue" disabled={!this.props.active} onClick={this.handleApply}> - { this.props.active ? 'Daftar' : 'Sudah Terdaftar' } <Icon name="right chevron" /> - </Button> - </Modal.Actions> - </Modal> - ) + )} + </Modal.Content> + <Modal.Actions> + <Button loading={this.state.load} color="blue" disabled={!this.props.active} onClick={this.handleApply}> + { this.props.active ? 'Daftar' : 'Sudah Terdaftar' } <Icon name="right chevron" /> + </Button> + </Modal.Actions> + </Modal> + ); + } } diff --git a/assets/js/components/ApproveModal.jsx b/assets/js/components/ApproveModal.jsx index 132083a6b9dcaa27338a0b8c8e95b4af37f310a4..fa04bed2af8c90b015b45b1f7e0fdcf7780b5bea 100644 --- a/assets/js/components/ApproveModal.jsx +++ b/assets/js/components/ApproveModal.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Modal, Button } from 'semantic-ui-react'; +import { Modal, Button, Icon, Segment } from 'semantic-ui-react'; import Server from './../lib/Server'; import ConfirmationModal from './../components/ConfirmationModal'; import Applicant from './../components/Applicant'; @@ -21,6 +21,7 @@ export default class ApproveModal extends React.Component { this.handleOpen = this.handleOpen.bind(this); this.reject = this.reject.bind(this); this.accept = this.accept.bind(this); + this.gotoStudentProfile = this.gotoStudentProfile.bind(this); this.gotoStudentResume = this.gotoStudentResume.bind(this); this.gotoStudentTranscript = this.gotoStudentTranscript.bind(this); } @@ -69,6 +70,8 @@ export default class ApproveModal extends React.Component { gotoStudentTranscript = () => this.gotoLink(`/transcript/${this.props.data.id}`); + gotoStudentProfile = () => this.gotoLink(`/mahasiswa/${this.props.data.student.id}`); + accept = () => { this.modal.open( 'Terima Lamaran?', @@ -89,15 +92,25 @@ export default class ApproveModal extends React.Component { <Modal.Header>Data Lamaran</Modal.Header> <Modal.Content> <h4> Cover Letter </h4> - { this.props.data.cover_letter ? this.props.data.cover_letter : 'Kosong' } - <div style={{ float: 'right', textAlign: 'right' }}> - {this.props.data.student.resume ? <a onClick={this.gotoStudentResume} href="#" >CV Pelamar </a> : ''} - <br /> - {this.props.data.student.show_transcript ? <a onClick={this.gotoStudentTranscript} href="#" >Transkrip Pelamar</a> : ''} - <br /> + <Segment> + <p> + { this.props.data.cover_letter ? this.props.data.cover_letter : 'Kosong' } + </p> + </Segment> + <br /> + <div> + <b> + {this.props.data.student.resume ? <a onClick={this.gotoStudentResume} href="#" >CV Pelamar </a> : 'Pelamar tidak memiliki CV'} + <br /> + {this.props.data.student.show_transcript ? <a onClick={this.gotoStudentTranscript} href="#" >Transkrip Pelamar</a> : 'Pelamar tidak mengijinkan transktip dilihat'} + <br /> + </b> </div> </Modal.Content> <Modal.Actions> + <Button color="facebook" onClick={this.gotoStudentProfile} floated="left" > + <Icon name="user outline" /> Lihat Profil + </Button> <Button.Group> <Button disabled={this.props.data.status === Applicant.APPLICATION_STATUS.REJECTED} loading={this.state.rejectLoading} color="red" onClick={this.reject}>Tolak Lamaran</Button> <Button.Or /> diff --git a/assets/js/components/CompanyVacancy.jsx b/assets/js/components/CompanyVacancy.jsx index 8ff71d10692b46767974fb15ae97d2c61abdb424..d935ff7f095833e41d76aa6b9319edd30bab50d0 100644 --- a/assets/js/components/CompanyVacancy.jsx +++ b/assets/js/components/CompanyVacancy.jsx @@ -2,6 +2,7 @@ import React from 'react'; import moment from 'moment'; import { Button, Icon, Item, Grid } from 'semantic-ui-react'; import { Link } from 'react-router'; +import Server from '../lib/Server'; const defaultImage = 'http://semantic-ui.com/images/wireframe/image.png'; @@ -15,8 +16,12 @@ export default class CompanyVacancy extends React.Component { constructor(props) { super(props); + /* istanbul ignore next */ moment.locale('id'); - this.state = { deleteLoading: false }; + this.state = { deleteLoading: false, count: 0, countNew: 0 }; + Server.get(`/vacancies/${this.props.data.id}/count/`, false).then((data) => { + this.setState({ count: data.count, countNew: data.count_new }); + }); } getLink = `/buat-lowongan/${this.props.data.id}`; @@ -25,12 +30,13 @@ export default class CompanyVacancy extends React.Component { return ( <Item className="applicantItems"> <Item.Image src={this.props.data.company.logo ? this.props.data.company.logo : defaultImage} size="small" /> - <Item.Content> + <Item.Content verticalAlign="middle" style={{ wordWrap: 'break-word', width: '100%' }} > <Item.Header as="a">{this.props.data.name}</Item.Header> <Grid.Row> <Grid.Column floated="left"> - <h5> 105 Pendaftar </h5> - Ditutup {moment(moment(this.props.data.close_time)).fromNow()} + <p>{ this.state.count } Pendaftar<br/> + { this.state.countNew } Pendaftar Baru<br/><br/> + Ditutup {moment(moment(this.props.data.close_time)).fromNow()}</p> </Grid.Column> <Grid.Column floated="right"> {this.props.data.verified ? diff --git a/assets/js/components/VacancyList.jsx b/assets/js/components/VacancyList.jsx index 3522d6766b0972a55a2776039b0f56002208dcc3..3f6e1338ecb5f62abb402bce05e01e8997371411 100644 --- a/assets/js/components/VacancyList.jsx +++ b/assets/js/components/VacancyList.jsx @@ -3,6 +3,7 @@ import { Item, Button, Grid } from 'semantic-ui-react'; import { Link } from 'react-router'; import Vacancy from './Vacancy'; import CompanyVacancy from './CompanyVacancy'; +import AdminVacancy from './AdminVacancy'; import Server from '../lib/Server'; export default class VacancyList extends React.Component { @@ -17,6 +18,15 @@ export default class VacancyList extends React.Component { items: [], }; + updateStatus(id, status) { + const obj = []; + this.state.vacancies.map((vacancy) => { + if (vacancy.id !== id) return obj.push(vacancy); + return null; + }); + this.setState({ vacancies: obj }); + } + constructor(props) { super(props); /* istanbul ignore next */ @@ -27,6 +37,7 @@ export default class VacancyList extends React.Component { loading: true, }; this.generateVacancies = this.generateVacancies.bind(this); + this.updateStatus = this.updateStatus.bind(this); } deleteVacancy = id => Server.delete(`/vacancies/${id}/`, this.state).then(() => { @@ -53,7 +64,15 @@ export default class VacancyList extends React.Component { ), ); } - + if (this.props.user.role === 'admin' || this.props.user.role === 'supervisor'){ + return this.state.vacancies.map(vacancy => (<AdminVacancy + key={vacancy.id} + data={vacancy} + updateStatus={this.updateStatus} + />), + ); + } + return this.state.vacancies.map(vacancy => (<CompanyVacancy key={vacancy.id} data={vacancy} diff --git a/assets/js/components/VerifyAdminModal.jsx b/assets/js/components/VerifyAdminModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b5dbccaae44bb770cecf5ae716537e317116acf2 --- /dev/null +++ b/assets/js/components/VerifyAdminModal.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Button, Header, Modal } from 'semantic-ui-react'; + +export default class VerifyAdminModal extends React.Component { + + state = { modalOpen: false } + + handleOpen = () => this.setState({ + modalOpen: true, + }); + + handleClose = () => this.setState({ + modalOpen: false, + }); + + render() { + return ( + + <Modal + trigger={ + <Button color="blue" icon="right chevron" labelPosition="right" floated="right" content="ubah" onClick={this.handleOpen} /> + } + closeIcon="close" + open={this.state.modalOpen} + onClose={this.handleClose} + > + <Modal.Header>Software Engineer</Modal.Header> + <Modal.Content > + <Modal.Description> + <Header>Deskripsi Lowongan</Header> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, + sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. + </Modal.Description> + </Modal.Content> + + <Modal.Actions> + <Button color="green" floated="right" onClick={this.handleClose}>Verifikasi</Button> + </Modal.Actions> + + </Modal> + ); + } +} diff --git a/assets/js/index.jsx b/assets/js/index.jsx index 218b69334edcc59ab0f941aa96deac747b464a4f..accbc110ee314f1af10c5bd45d318e45d101648e 100644 --- a/assets/js/index.jsx +++ b/assets/js/index.jsx @@ -13,7 +13,7 @@ import ApplicantPage from './ApplicantPage'; import TranscriptPage from './TranscriptPage'; import AdminVacancyPage from './AdminVacancyPage'; import CompanyPage from './CompanyPage'; -import SupervisorPage from './SupervisorPage' +import SupervisorPage from './SupervisorPage'; export default class App extends React.Component { @@ -84,6 +84,9 @@ export default class App extends React.Component { } else if (App.getRole() === 'admin') { replace({ pathname: '/perusahaan' }); cb(); + } else if (App.getRole() === 'supervisor') { + replace({ pathname: '/lowongan' }); + cb(); } } replace({ pathname: '/login' }); cb(); @@ -97,12 +100,12 @@ export default class App extends React.Component { const commonUser = this.authorization(['admin', 'student', 'company']); const grownups = this.authorization(['admin', 'company', 'supervisor']); const facultyMember = this.authorization(['admin', 'student', 'supervisor']); - const all = this.authorization(['admin', 'company', 'student', 'supervisor']); + const all = this.authorization(['admin', 'company', 'supervisor', 'student']); return ( <Router history={browserHistory}> <Route path="/login" component={Login} /> - <Route component={commonUser(Dashboard)} onEnter={this.handleAuth}> + <Route component={all(Dashboard)} onEnter={this.handleAuth}> <Route path="/transcript/:id" component={company(TranscriptPage)} /> <Route path="/lowongan" component={commonUser(VacancyPage)} /> <Route path="/admin-lowongan" component={AdminVacancyPage} /> @@ -117,6 +120,7 @@ export default class App extends React.Component { <Route path="/transkrip/:id" component={facultyMember(CompanyProfile)} /> </Route> <Route path="/home" onEnter={this.handleHome} /> + <Route path="/admin-vacancy" component={VacancyPage} /> <Redirect from="*" to="/home" /> </Router> ); diff --git a/core/lib/permissions.py b/core/lib/permissions.py index 3cd5342c95b0dbf711c7a4ab42e1c14e54bb2cf4..0e85f561def05dbec4d981f3c182943fa3104e46 100644 --- a/core/lib/permissions.py +++ b/core/lib/permissions.py @@ -5,6 +5,7 @@ from core.models import Company from core.models import Student from core.models import Supervisor from core.models import Application +from core.models import Vacancy def is_admin_or_student(user): @@ -146,7 +147,13 @@ class IsAdminOrVacancyOwner(permissions.BasePermission): "Checking owner permission on non-application object" ) - class AsAdminOrSupervisor(permissions.BasePermission): def has_permission(self, request, view): return is_admin_or_supervisor(request.user) + +class VacancyApprovalPermission(permissions.BasePermission): + def has_permission(self, request, view): + return is_admin_or_supervisor(request.user) + + def has_object_permission(self, request, view, obj): + return isinstance(obj, Vacancy) diff --git a/core/serializers/accounts.py b/core/serializers/accounts.py index 25440942f2b519fd502ebb6dac6b6eb1d3b84b6e..cd98fdb05f2ce63acfd11491d3bfa9ffe0cb5764 100644 --- a/core/serializers/accounts.py +++ b/core/serializers/accounts.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User from rest_framework import serializers from core.models.accounts import Supervisor, Company, Student - +from core.models.vacancies import Application class BasicUserSerializer(serializers.HyperlinkedModelSerializer): class Meta: @@ -13,11 +13,17 @@ class BasicUserSerializer(serializers.HyperlinkedModelSerializer): class StudentSerializer(serializers.ModelSerializer): user = BasicUserSerializer() name = serializers.ReadOnlyField() + accepted_no = serializers.SerializerMethodField() class Meta: model = Student fields = ['id', 'name', 'user', 'npm', 'resume', 'phone_number', 'birth_place', 'birth_date', 'major', 'batch', \ - 'show_transcript', 'photo'] + 'show_transcript', 'photo', 'accepted_no'] + + def get_accepted_no(self, obj): + apps = Application.objects.filter(student=obj, status=4) + companies = apps.values('vacancy__company').distinct() + return companies.count() class StudentUpdateSerializer(serializers.ModelSerializer): diff --git a/core/serializers/vacancies.py b/core/serializers/vacancies.py index da3d5b101e4e6433a326c6a1e9bd46aa970e642d..313f2064622806a59994a83eba2ea82f0eb7a4a8 100644 --- a/core/serializers/vacancies.py +++ b/core/serializers/vacancies.py @@ -59,7 +59,6 @@ class ApplicationStatusSerializer(serializers.ModelSerializer): model = Application fields = ['status'] - class SupervisorStudentApplicationSerializer(serializers.ModelSerializer): def to_representation(self, instance): status_map = ["new", "read", "bookmarked", "rejected", "accepted" ] @@ -76,3 +75,16 @@ class SupervisorStudentApplicationSerializer(serializers.ModelSerializer): fields = ['name', 'npm', 'vacancy_name', 'company_name', 'status'] read_only_fields = ['name', 'npm', 'vacancy_name', 'company_name', 'status'] +class VacancyApplicationSerializer(serializers.ModelSerializer): + vacancy = VacancySerializer() + + class Meta: + model = Application + fields = ['cover_letter', 'vacancy', 'status'] + + +class VacancyVerifiedSerializer(serializers.ModelSerializer): + + class Meta: + model = Vacancy + fields = ['verified'] diff --git a/core/templates/core/index.html b/core/templates/core/index.html index ebdfabf2c40d38ef365e4bb52b22a83a1a45d388..da540334ff89edaa8b744dcc9c13774b48d719b0 100755 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -8,6 +8,9 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Yuk Cari Tempat Kape :)</title> <link rel="stylesheet" href="{% static 'css/custom.css' %}"/> + <link rel="stylesheet" href="../../../node_modules/react-quill/dist/quill.snow.css"> + <script src="https://cdn.ckeditor.com/4.6.2/standard/ckeditor.js"></script> + <!--<script src="https://cloud.tinymce.com/stable/tinymce.min.js"></script>--> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.2/semantic.min.css"/> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/react-datepicker/0.44.0/react-datepicker.min.css"/> <link rel="icon" type="image/png" href="{% static 'img/logo-sm.png'%}" sizes="32x32" /> diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py index 14da0a242baf57ab719354b35600778a2580c2ec..a780001f6ad07ef6c60b4f21f070e1ad74a3cb14 100644 --- a/core/tests/test_vacancies.py +++ b/core/tests/test_vacancies.py @@ -187,10 +187,13 @@ class CompanyListsTests(APITestCase): response = self.client.post(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - class SupervisorStudentApplicationTests(APITestCase): def test_list_student_application(self): + +class SupervisorApprovalTests(APITestCase): + + def test_supervisor_approve_vacancy(self): new_user = User.objects.create_user('dummy.supervisor', 'dummy.supervisor@asd.asd', 'lalala123') new_supervisor = Supervisor.objects.create(user=new_user, nip=1212121212) self.client.force_authenticate(user=new_user) @@ -206,3 +209,30 @@ class SupervisorStudentApplicationTests(APITestCase): url = '/api/student-applications/' response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + new_user2 = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123') + new_company2 = Company.objects.create(user=new_user2, description="lalala", status=Company.VERIFIED, logo=None, + address=None) + new_vacancy2 = Vacancy.objects.create(company=new_company2, verified=False, open_time=datetime.fromtimestamp(0), + description="lalala", close_time=datetime.today()) + + url = '/api/vacancies/' + str(new_vacancy2.pk) + '/verify/' + response = self.client.patch(url, {'verified' : True}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + retrieve_vacancy = Vacancy.objects.get(pk=new_vacancy2.pk) + self.assertEqual(retrieve_vacancy.verified, True) + + def test_unauthorized_approve_vacancy(self): + new_user = User.objects.create_user('dummy.companyz', 'dummy.companyz@company.com', 'lalala123') + new_company = Company.objects.create(user=new_user, description="lalalaz", status=Company.VERIFIED, logo=None, + address=None) + self.client.force_authenticate(user=new_user) + + new_vacancy = Vacancy.objects.create(company=new_company, verified=False, open_time=datetime.fromtimestamp(0), + description="lalala", close_time=datetime.today()) + + url = '/api/vacancies/' + str(new_vacancy.pk) + '/verify/' + response = self.client.patch(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(new_vacancy.verified, False) diff --git a/core/views/vacancies.py b/core/views/vacancies.py index 66c8429323ac5b8734d98f6a017a70d14b121b38..ead62fd6f407de5d99a85c91a6fad5b8c3e633c8 100644 --- a/core/views/vacancies.py +++ b/core/views/vacancies.py @@ -8,11 +8,11 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.pagination import PageNumberPagination from core.lib.mixins import MultiSerializerViewSetMixin -from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVacancyOwner, AsAdminOrSupervisor +from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany, IsAdminOrVacancyOwner, AsAdminOrSupervisor, VacancyApprovalPermission from core.models import Student, Company from core.models.vacancies import Vacancy, Application from core.serializers.vacancies import VacancySerializer, ApplicationSerializer, ApplicationStatusSerializer, \ - PostVacancySerializer, SupervisorStudentApplicationSerializer + VacancyApplicationSerializer, PostVacancySerializer, VacancyVerifiedSerializer class VacancyViewSet(MultiSerializerViewSetMixin, viewsets.ModelViewSet): @@ -34,6 +34,8 @@ class VacancyViewSet(MultiSerializerViewSetMixin, viewsets.ModelViewSet): verified = request.query_params['verified'] if 'verified' in request.query_params else "True" if verified.lower() in ("yes", "true", "t", "1"): vacancies = vacancies.filter(verified=True) + if verified.lower() in {"no", "false", "f", "0"}: + vacancies = vacancies.filter(verified=False) page = self.paginate_queryset(vacancies) if page is not None: return self.get_paginated_response(VacancySerializer(page, many=True, context={'request': request}).data) @@ -46,6 +48,15 @@ class VacancyViewSet(MultiSerializerViewSetMixin, viewsets.ModelViewSet): count_new = Application.objects.filter(vacancy=vacancy, status=Application.NEW).count() return Response({"count": count, "count_new": count_new}, status=status.HTTP_200_OK) + @detail_route(methods=['patch'], permission_classes=[VacancyApprovalPermission], serializer_class=VacancyVerifiedSerializer) + def verify(self, request, pk=None): + vacancy = self.get_object() + serializer = self.get_serializer_class()(vacancy, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response({"error" : "bad request"}, status=status.HTTP_400_BAD_REQUEST) + class ApplicationViewSet(viewsets.GenericViewSet): serializer_class = ApplicationSerializer diff --git a/package.json b/package.json index ccd3cef7247924cd935e1083793866dcf9f9b30e..f103a60db5aa9c6d31fd2c338f98d379ece26df4 100755 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "object-assign": "^4.1.1", "react": "15.4.2", "react-addons-test-utils": "15.4.2", + "react-ckeditor-wrapper": "^1.0.22", "react-datepicker": "^0.44.0", "react-dom": "15.4.2", "react-router": "^3.0.2",