diff --git a/.eslintrc b/.eslintrc index 3832227820fa2ff722a19b1f54c1c2d83047074c..4f0655a0efe2b12a7baeae04dd53ae1fed30c849 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,6 +11,7 @@ "func-names": ["error", "never"], "react/prefer-stateless-function": [0, { "ignorePureComponents": true }], "react/forbid-prop-types": [0], + "react/no-multi-comp": [0, { "ignoreStateless": 1 }], "import/extensions": ["off", "never"], "import/no-unresolved": 0, "no-underscore-dangle" : 0, diff --git a/.gitignore b/.gitignore index 99d047182404056b599c0959868416ddbbf4ef7f..e52ab6277ed4e707ea668f8df9b55d465f6e0bfb 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test/* .coverage .tmp/ npm-debug.log +debug.log diff --git a/assets/css/custom.css b/assets/css/custom.css index 660c7f39a35a3aeee306a65e7664ab78eff16a05..f49838bc0f59ea2b9c01b28a4b9734c97df7ccab 100755 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,41 +1,41 @@ - - -body{ - padding: 0; - height: auto; - width: auto; - background-color: black; -} - .center{ text-align: center; } - * { box-sizing: border-box; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; } -body { - font: 300 14px/1.4 'Helvetica Neue', Helvetica, Arial, sans-serif; - margin: 0; - padding: 0; + +.applicant{ + margin-left: 150px; + margin-right: 150px; +} + +.ui.inverted.segment.header{ + background-color: #EEEEEE; + color:black; } -.halamanLowongan{ +.ui.card.register{ +background-color: #EEEEEE; } +.create-lowongan{ + margin: 60px 19% 100px; +} + +.ui.segment.form-segment{ + padding-bottom: 50px; +} .tabs { - margin-top: 100px; - margin-left: 150px; - margin-right: 150px; background: #fff; border: 1px solid #e5e5e5; border-radius: 3px; - margin-bottom: 30px; + margin: 100px 100px 30px; } .tabs__labels { margin: 0; @@ -59,29 +59,23 @@ body { } .halamanLogin{ - background-image: url("../img/bw.jpg"); background-size: cover; - background-position: center; - background-attachment: fixed; - height: 700px; + background: url("../img/background.png")no-repeat center center fixed; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + padding-bottom: 80px; } -.register{ -margin-left:180px; -margin-right:110px; +.headerLoginform{ + background-color: #2e6da4; } .formLogin{ -margin: 0 auto; -margin-top: 100px; -width: 430px; -border: 2px solid transparent; -border-radius: 12px; -border-color:#e8e8e8; -padding: 20px; - background-color: white; - + margin: 30px auto 0; + width: 430px; } .registerModal{ @@ -95,7 +89,6 @@ padding: 20px; .formLogin img{ height: 70px !important; margin:5px; -margin-bottom:30px; } .formLogin span{ @@ -115,7 +108,7 @@ font-size: 32px; } .daftar{ float: right; - margin-top:10px; + margin-top:10px; } .coverLetter{ @@ -129,23 +122,24 @@ font-size: 32px; margin-bottom: 10px; } + .registerForm{ margin: 0 auto; - padding:50px; + /*padding:50px;*/ background-color: #3B8686; - } -item{ +h3{ color:black; } -textarea{ - width: 800px !important; - height: 160px !important; +h4{ + color: black; } - +h5{ + color: black; +} .note { color: gray; @@ -170,7 +164,19 @@ card .formRegis{ margin-top: 100px; } -itemLowongan{ +.itemLowongan{ + color: black; +} + +.ui.pointing.secondary.menu{ + background-color: #ffffff; +} + +.extra.extra-company h3{ + padding-top:10px; +} + +.extra.extra-company h2{ color: black; } diff --git a/assets/img/background.png b/assets/img/background.png new file mode 100644 index 0000000000000000000000000000000000000000..078c73fd00f1115eb1d204a3c6d6a7cd32467a76 Binary files /dev/null and b/assets/img/background.png differ diff --git a/assets/img/background2.png b/assets/img/background2.png new file mode 100644 index 0000000000000000000000000000000000000000..246cb7ee4a5100da50ebe27175edd7ba43158548 Binary files /dev/null and b/assets/img/background2.png differ diff --git a/assets/img/bw-blur.png b/assets/img/bw-blur.png new file mode 100644 index 0000000000000000000000000000000000000000..47a69c72ede54ce8bad52796f8af4f6db16a12d9 Binary files /dev/null and b/assets/img/bw-blur.png differ diff --git a/assets/js/ApplicantPage.jsx b/assets/js/ApplicantPage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..51334b1d73700944632bff36164b92e0c7e7e650 --- /dev/null +++ b/assets/js/ApplicantPage.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Tabs from './components/Tabs'; +import Pane from './components/Pane'; +import Storage from './lib/Storage'; +import VacancyList from './components/VacancyList'; + +export default class VacancyPage extends React.Component { + + constructor(props) { + super(props); + /* istanbul ignore next */ + this.state = { + email: '', + password: '', + errorFlag: false, + vacancies: [], + }; + this.handleItemClick = this.handleItemClick.bind(this); + } + + handleItemClick = (e, { name }) => this.setState({ activeItem: name }); + + render() { + const student = Storage.get('user-data').student; + return ( + <div className="halamanPendaftar"> + <Tabs selected={0}> + <Pane label="Lamaran Baru" > + <VacancyList key={1} studentId={student.id} url="/vacancies/" /> + </Pane> + <Pane label="Bintangi" > + <VacancyList key={2} status="Batal" studentId={student.id} url={`/students/${student.id}/applied-vacancies/`} /> + </Pane> + <Pane label="Semua Lamaran" > + <VacancyList key={3} studentId={student.id} url={`/students/${student.id}/bookmarked-vacancies/`} /> + </Pane> + </Tabs> + + </div> + + ); + } +} diff --git a/assets/js/CreateVacancy.jsx b/assets/js/CreateVacancy.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e5015d6ff56edaa2b97e3b7ab0ab69f6b590368d --- /dev/null +++ b/assets/js/CreateVacancy.jsx @@ -0,0 +1,109 @@ +import React from 'react'; +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 ModalAlert from './components/ModalAlert'; +import Server from './lib/Server'; +import Storage from './lib/Storage'; +import Dumper from './lib/Dumper'; + +export default class CreateVacancy extends React.Component { + + static propTypes = { + params: React.PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + /* istanbul ignore next */ + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + formLoading: false, + company: Storage.get('user-data').company, + vacancyId: this.props.params.id, + open_time: moment(), + close_time: moment(), + name: '', + 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), + }); + }); + } + + handleChange = (e) => { + this.setState({ [e.target.name]: e.target.value }); + }; + + handleSubmit = (e) => { + e.preventDefault(); + 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 url = this.state.vacancyId ? `/vacancies/${this.state.vacancyId}/` : '/vacancies/'; + const method = this.state.vacancyId ? 'PATCH' : 'POST'; + + Server.sendRequest(url, method, data).then(() => { + browserHistory.push('/lowongan'); + }, error => error.then((r) => { + this.modalAlert.open('Gagal Membuat Lowongan', Dumper.dump(r, ' ')); + this.setState({ formLoading: false }); + })); + }; + + render = () => ( + <div className="create-lowongan" > + <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> + <Segment className="form-segment"> + <Header as="h2" icon textAlign="center"> + <Icon name="briefcase" circular /> + <Header.Content> + Lowongan KP + </Header.Content> + </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 + /> + <Form.Group widths="equal"> + <Form.Field + className="open-time-field" + control={DatePicker} + label="Waktu Buka Lowongan" + selected={this.state.open_time} + onChange={date => this.setState({ open_time: date })} + required + /> + <Form.Field + className="close-time-field" + control={DatePicker} + label="Waktu Tutup Lowongan" + selected={this.state.close_time} + onChange={date => this.setState({ close_time: date })} + required + /> + </Form.Group> + <Button type="submit" primary floated="right">Submit</Button> + </Form> + </Segment> + </div> + ); +} diff --git a/assets/js/Login.jsx b/assets/js/Login.jsx index 773e0cabd1292dc331171f03cfc0ec2712ce88c5..0065df988f8b5d93448959b78db5714a2e522a5f 100644 --- a/assets/js/Login.jsx +++ b/assets/js/Login.jsx @@ -1,7 +1,7 @@ import React from 'react'; -import { Grid, Segment, Card } from 'semantic-ui-react'; +import { Grid, Segment, Header, Card, Image } from 'semantic-ui-react'; import LoginForm from './components/LoginForm'; -import RegisterModal from './components/RegisterModal'; +import CompanyRegisterModal from './components/CompanyRegisterModal'; export default class Login extends React.Component { @@ -17,26 +17,37 @@ export default class Login extends React.Component { }; render = () => ( - <div className="halamanLogin"> - <Grid columns={2} relaxed> + <div className="headerLogin"> + <Header as="h2" icon textAlign="center" > + <Image src="/assets/img/logo.png" size="medium" centered /> + <Header.Content > + Kanal Akses Pendaftaran KP Elektronik + </Header.Content> + </Header> + </div> + + <Grid stackable columns={2} relaxed> <Grid.Column> <Segment basic> <LoginForm type="company" header="Company Login" imgSrc="logo.png" imgSize="small" /> {this.props.children} - </Segment > + <div className="register"> - <Card> + <Card centered className="register" > <Card.Content> - <Card.Header> - New to us ? - </Card.Header> - <RegisterModal /> + <Grid columns={2} relaxed> + <Grid.Column> + <Header as="h3">New to us ?</Header> + </Grid.Column> + <Grid.Column> + <CompanyRegisterModal /> + </Grid.Column> + </Grid> </Card.Content> </Card> </div> - </Grid.Column> <Grid.Column> @@ -45,7 +56,6 @@ export default class Login extends React.Component { {this.props.children} </Segment> </Grid.Column> - </Grid> </div> ) diff --git a/assets/js/VacancyPage.jsx b/assets/js/VacancyPage.jsx index cb25fd829a32bea33aaa78021f24f6ae4751f51a..551033a1c815bc24ccc5f9813dc5591b18b86ad7 100644 --- a/assets/js/VacancyPage.jsx +++ b/assets/js/VacancyPage.jsx @@ -6,38 +6,78 @@ import VacancyList from './components/VacancyList'; export default class VacancyPage extends React.Component { + static getRole() { + const student = Storage.get('user-data').student; + const company = Storage.get('user-data').company; + if (student) { + return { id: student.id, type: 'student' }; + } else if (company) { + return { id: company.id, type: 'company' }; + } + + return { id: '0', type: 'error' }; + } + constructor(props) { super(props); /* istanbul ignore next */ + const role = VacancyPage.getRole(); this.state = { - email: '', - password: '', - errorFlag: false, vacancies: [], - student: false, + id: role.id, + role: role.type, }; - Storage.getUserData().then(r => this.setState({ student: r.student })); - - this.handleItemClick = this.handleItemClick.bind(this); } - handleItemClick = (e, { name }) => this.setState({ activeItem: name }); - - render = () => ( - this.state.student && ( - <div className="halamanLowongan"> + generateVacancies() { + if (this.state.role === 'student') { + return ( <Tabs selected={0}> <Pane label="Semua Lowongan" > - <VacancyList key={1} studentId={this.state.student.id} url="/vacancies/" /> + <VacancyList + key={1} + userId={this.state.id} + url="/vacancies/" + /> </Pane> <Pane label="Lamaran saya" > - <VacancyList key={2} status="Batal" studentId={this.state.student.id} url={`/students/${this.state.student.id}/applied-vacancies/`} /> + <VacancyList + key={2} + status="Batal" + userId={this.state.id} + url={`/students/${this.state.id}/applied-vacancies/`} + /> </Pane> <Pane label="Lamaran Ditandai" > - <VacancyList key={3} studentId={this.state.student.id} url={`/students/${this.state.student.id}/bookmarked-vacancies/`} /> + <VacancyList + key={3} + userId={this.state.id} + url={`/students/${this.state.id}/bookmarked-vacancies/`} + /> </Pane> </Tabs> + ); + } else if (this.state.role === 'company') { + return ( + <VacancyList key={1} userId={this.state.id} url={`/companies/${this.state.id}/vacancies/`} type="company" /> + ); + } + + return ( + <div> + <h3> + Anda tidak terautentifikasi. Harap logout dan login + kembali dengan akun yang benar + </h3> </div> - ) - ); + ); + } + + render() { + return ( + <div className="applicant"> + { this.generateVacancies() } + </div> + ); + } } diff --git a/assets/js/__test__/CreateVacancy-test.jsx b/assets/js/__test__/CreateVacancy-test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..82c288168bc8b8a2704876644b4434521b2c53ce --- /dev/null +++ b/assets/js/__test__/CreateVacancy-test.jsx @@ -0,0 +1,164 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import fetchMock from 'fetch-mock'; +import CreateVacancy from '../CreateVacancy'; +import Storage from '../lib/Storage'; + +describe('CreateVacancy', () => { + const companySession = { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + company: { + id: 3, + user: { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + }, + name: 'Tutuplapak', + created: '2017-03-28T07:30:10.535000Z', + updated: '2017-03-28T07:30:10.535000Z', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla aliquet semper neque a fermentum. Duis ac tellus vitae augue iaculis ultrices. Curabitur commodo et neque nec feugiat. Morbi ac diam vel nunc commodo cursus. Phasellus nulla sapien, hendrerit vitae bibendum at, sollicitudin eu ante. Maecenas maximus, ante eu sollicitudin convallis, mauris nunc posuere risus, eu porttitor diam lacus vitae enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse at lectus a elit sollicitudin tempor. Nullam condimentum, justo nec tincidunt maximus, neque mi vulputate leo, sit amet lacinia massa ex eget sem. Duis ac erat facilisis, fringilla mauris in, consequat neque. In et neque consequat, vehicula magna at, efficitur ante. Mauris ac lacinia nibh.\r\n\r\nProin sagittis, lectus quis maximus varius, libero justo sollicitudin augue, non lacinia risus orci a enim. Curabitur iaculis enim quis ullamcorper commodo. Vivamus id nisi rhoncus, dignissim tellus quis, interdum est. Fusce sollicitudin eu libero ac feugiat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Maecenas semper posuere ex, sed accumsan libero iaculis faucibus. Fusce laoreet ac ligula ut consectetur. Donec tortor mauris, rutrum at sodales et, viverra in dolor. Sed bibendum elit et maximus volutpat. Phasellus justo ipsum, laoreet sit amet faucibus eu, ultricies suscipit mauris. Nullam aliquam libero eu ante ultrices mattis. Donec non justo hendrerit neque volutpat placerat. Ut euismod est nec sem mollis, sit amet porttitor massa rhoncus. Aenean id erat sit amet nunc ultrices scelerisque non in ipsum. Curabitur sollicitudin nulla id mi accumsan venenatis.', + verified: true, + logo: 'http://localhost:8001/files/company-logo/8a258a48-3bce-4873-b5d1-538b360d0059.png', + address: 'Jl. Kebayoran Baru nomor 13, Jakarta Barat', + }, + supervisor: null, + student: null, + }; + + const errorSession = { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + company: null, + supervisor: null, + student: null, + }; + + it('renders for companies without problem', () => { + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: 1 }} />, + ); + expect(createVacancy).to.exist; + fetchMock.restore(); + }); + + + it('renders without problem for error case', () => { + fetchMock.get('*', {}); + Storage.set('user-data', errorSession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: 1 }} />, + ); + expect(createVacancy).to.exist; + fetchMock.restore(); + }); + + it('support handle change', () => { + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: 1 }} />, + ); + createVacancy.setState({ + name: 'stub', + description: 'stub', + }); + createVacancy.handleChange({ target: { name: 'test', value: 'hue' } }); + expect(createVacancy.state.test).to.equal('hue'); + fetchMock.restore(); + }); + + it('submit vacancy properly (loading)', () => { + fetchMock.post('*', 404); + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: undefined }} />, + ); + createVacancy.setState({ + name: 'stub', + description: 'stub', + }); + + const openField = ReactTestUtils.findRenderedDOMComponentWithClass(createVacancy, 'open-time-field'); + const closeField = ReactTestUtils.findRenderedDOMComponentWithClass(createVacancy, 'close-time-field'); + ReactTestUtils.Simulate.click(openField); + ReactTestUtils.Simulate.keyDown(openField, { key: 'Enter', keyCode: 13, which: 13 }); + ReactTestUtils.Simulate.click(closeField); + ReactTestUtils.Simulate.keyDown(closeField, { key: 'Enter', keyCode: 13, which: 13 }); + + expect(createVacancy.state.formLoading).to.equal(false); + createVacancy.handleSubmit(new Event('click')); + expect(createVacancy.state.formLoading).to.equal(true); + fetchMock.restore(); + }); + + it('submit vacancy properly (success)', () => { + fetchMock.post('*', { data: 'value' }); + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: undefined }} />, + ); + createVacancy.setState({ + name: 'stub', + description: 'stub', + }); + + expect(createVacancy.state.formLoading).to.equal(false); + createVacancy.handleSubmit(new Event('click')); + expect(createVacancy.state.formLoading).to.equal(true); + fetchMock.restore(); + }); + + it('submit vacancy properly (loading)', () => { + fetchMock.patch('*', 404); + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: 1 }} />, + ); + createVacancy.setState({ + name: 'stub', + description: 'stub', + }); + + const openField = ReactTestUtils.findRenderedDOMComponentWithClass(createVacancy, 'open-time-field'); + const closeField = ReactTestUtils.findRenderedDOMComponentWithClass(createVacancy, 'close-time-field'); + ReactTestUtils.Simulate.click(openField); + ReactTestUtils.Simulate.keyDown(openField, { key: 'Enter', keyCode: 13, which: 13 }); + ReactTestUtils.Simulate.click(closeField); + ReactTestUtils.Simulate.keyDown(closeField, { key: 'Enter', keyCode: 13, which: 13 }); + + expect(createVacancy.state.formLoading).to.equal(false); + createVacancy.handleSubmit(new Event('click')); + expect(createVacancy.state.formLoading).to.equal(true); + fetchMock.restore(); + }); + + it('submit vacancy properly (success)', () => { + fetchMock.patch('*', { data: 'value' }); + fetchMock.get('*', {}); + Storage.set('user-data', companySession); + const createVacancy = ReactTestUtils.renderIntoDocument( + <CreateVacancy params={{ id: 1 }} />, + ); + createVacancy.setState({ + name: 'stub', + description: 'stub', + }); + + expect(createVacancy.state.formLoading).to.equal(false); + createVacancy.handleSubmit(new Event('click')); + expect(createVacancy.state.formLoading).to.equal(true); + fetchMock.restore(); + }); +}); diff --git a/assets/js/__test__/VacancyPage-test.jsx b/assets/js/__test__/VacancyPage-test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..df655ca49d1baf7254b6bee70b9246a32be47ea2 --- /dev/null +++ b/assets/js/__test__/VacancyPage-test.jsx @@ -0,0 +1,101 @@ +/* eslint-disable no-unused-expressions */ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import fetchMock from 'fetch-mock'; +import VacancyPage from '../VacancyPage'; +import Storage from '../lib/Storage'; + +describe('VacancyPage', () => { + fetchMock.get('*', { data: 'value' }); + + const studentSession = { + url: 'http://localhost:8000/api/users/9/', + username: 'muhammad.reza42', + email: 'muhammad.reza42@ui.ac.id', + is_staff: false, + company: null, + supervisor: null, + 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, + bookmarked_vacancies: [ + 3, + ], + applied_vacancies: [ + 3, + 1, + ], + }, + }; + + const companySession = { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + company: { + id: 3, + user: { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + }, + name: 'Tutuplapak', + created: '2017-03-28T07:30:10.535000Z', + updated: '2017-03-28T07:30:10.535000Z', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla aliquet semper neque a fermentum. Duis ac tellus vitae augue iaculis ultrices. Curabitur commodo et neque nec feugiat. Morbi ac diam vel nunc commodo cursus. Phasellus nulla sapien, hendrerit vitae bibendum at, sollicitudin eu ante. Maecenas maximus, ante eu sollicitudin convallis, mauris nunc posuere risus, eu porttitor diam lacus vitae enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse at lectus a elit sollicitudin tempor. Nullam condimentum, justo nec tincidunt maximus, neque mi vulputate leo, sit amet lacinia massa ex eget sem. Duis ac erat facilisis, fringilla mauris in, consequat neque. In et neque consequat, vehicula magna at, efficitur ante. Mauris ac lacinia nibh.\r\n\r\nProin sagittis, lectus quis maximus varius, libero justo sollicitudin augue, non lacinia risus orci a enim. Curabitur iaculis enim quis ullamcorper commodo. Vivamus id nisi rhoncus, dignissim tellus quis, interdum est. Fusce sollicitudin eu libero ac feugiat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Maecenas semper posuere ex, sed accumsan libero iaculis faucibus. Fusce laoreet ac ligula ut consectetur. Donec tortor mauris, rutrum at sodales et, viverra in dolor. Sed bibendum elit et maximus volutpat. Phasellus justo ipsum, laoreet sit amet faucibus eu, ultricies suscipit mauris. Nullam aliquam libero eu ante ultrices mattis. Donec non justo hendrerit neque volutpat placerat. Ut euismod est nec sem mollis, sit amet porttitor massa rhoncus. Aenean id erat sit amet nunc ultrices scelerisque non in ipsum. Curabitur sollicitudin nulla id mi accumsan venenatis.', + verified: true, + logo: 'http://localhost:8001/files/company-logo/8a258a48-3bce-4873-b5d1-538b360d0059.png', + address: 'Jl. Kebayoran Baru nomor 13, Jakarta Barat', + }, + supervisor: null, + student: null, + }; + + const errorSession = { + url: 'http://localhost:8001/api/users/8/', + username: 'Tutuplapak', + email: '', + is_staff: false, + company: null, + supervisor: null, + student: null, + }; + + it('renders for companies without problem', () => { + Storage.set('user-data', companySession); + const vacancyPage = ReactTestUtils.renderIntoDocument( + <VacancyPage studentId={1} url="test" />); + expect(vacancyPage).to.exist; + expect(vacancyPage.state.role).to.equal('company'); + }); + + it('renders for students without problem', () => { + Storage.set('user-data', studentSession); + const vacancyPage = ReactTestUtils.renderIntoDocument( + <VacancyPage studentId={1} url="test" />); + expect(vacancyPage).to.exist; + expect(vacancyPage.state.role).to.equal('student'); + }); + + it('renders without problem for error case', () => { + Storage.set('user-data', errorSession); + const vacancyPage = ReactTestUtils.renderIntoDocument( + <VacancyPage studentId={1} url="test" />); + expect(vacancyPage).to.exist; + expect(vacancyPage.state.role).to.equal('error'); + }); +}); + diff --git a/assets/js/__test__/components/ApplyConfirmationModal-test.jsx b/assets/js/__test__/components/ApplyConfirmationModal-test.jsx index 2bec421863a4de808bb925c87e85bba167ae5a23..7e197f730f448182847b722db650fcc7624ef7c8 100644 --- a/assets/js/__test__/components/ApplyConfirmationModal-test.jsx +++ b/assets/js/__test__/components/ApplyConfirmationModal-test.jsx @@ -1,11 +1,11 @@ import React from 'react'; import ReactTestUtils from 'react-addons-test-utils'; import ApplyConfirmationModal from '../../components/ApplyConfirmationModal'; +import fetchMock from 'fetch-mock'; import Storage from '../../lib/Storage'; import Server from '../../lib/Server'; describe('ApplyConfirmationModal', () => { - const fetchMock = require('fetch-mock'); it('renders without problem', () => { const applyModal = ReactTestUtils.renderIntoDocument( @@ -26,6 +26,7 @@ describe('ApplyConfirmationModal', () => { applyModal.handleOpen(); applyModal.close(); expect(applyModal.state.header).to.equal('Menghubungkan ke Server'); + fetchMock.restore(); }); it('test apply with problem', () => { @@ -35,11 +36,11 @@ describe('ApplyConfirmationModal', () => { const response = { student: { id: 1, name: 2 } }; Storage.set('user-data', response); - fetchMock.post('*', { status: 404, body: response }); + fetchMock.post('*', 404); applyModal.open(); applyModal.handleOpen(); applyModal.close(); - expect(applyModal.state.header).to.equal('Menghubungkan ke Server'); + fetchMock.restore(); }); }); \ No newline at end of file diff --git a/assets/js/__test__/components/ApplyModal-test.jsx b/assets/js/__test__/components/ApplyModal-test.jsx index 58baa31d2d68f6438a27d7f6e0cf7eaeaaf6c0f0..f805b8dba8b9d81bfe2d2650c8f374eb41bf2ca6 100644 --- a/assets/js/__test__/components/ApplyModal-test.jsx +++ b/assets/js/__test__/components/ApplyModal-test.jsx @@ -4,11 +4,11 @@ import ReactTestUtils from 'react-addons-test-utils'; import ApplyModal from '../../components/ApplyModal'; describe('ApplyModal', () => { - it('renders without problem', () => { + it('renders without problem', () => { const modalPendaftaran = ReactTestUtils.renderIntoDocument( - <ApplyModal id={4} data={{key: 'value'}} buttonTitle="submit" />); + <ApplyModal id={4} data={{ key: 'value' }} buttonTitle="submit" />); expect(modalPendaftaran).to.exist; - }); + }); it('open without problem', () => { const modalPendaftaran = ReactTestUtils.renderIntoDocument( diff --git a/assets/js/__test__/components/CancelModal-test.jsx b/assets/js/__test__/components/CancelModal-test.jsx index 1ee7dcf69693e6f3df35b2cb12bae02a7310b163..831285bf22444338d37a0e5f6487ca1ab1bc5813 100644 --- a/assets/js/__test__/components/CancelModal-test.jsx +++ b/assets/js/__test__/components/CancelModal-test.jsx @@ -1,6 +1,7 @@ /* eslint-disable no-unused-expressions */ import React from 'react'; import ReactTestUtils from 'react-addons-test-utils'; +import fetchMock from 'fetch-mock'; import CancelModal from '../../components/CancelModal'; import Storage from '../../lib/Storage'; @@ -29,7 +30,6 @@ describe('CancelModal', () => { }); it('remove vacancy without problem', () => { - const fetchMock = require('fetch-mock'); fetchMock.delete('*', { data: 'value' }); const modalPendaftaran = ReactTestUtils.renderIntoDocument( <CancelModal id={4} />); @@ -38,21 +38,32 @@ describe('CancelModal', () => { Storage.set('user-data', response3); modalPendaftaran.removeVacancy(); expect(modalPendaftaran.state.header).to.exist; + fetchMock.restore(); }); - it('confirm without problem', () => { - const fetchMock = require('fetch-mock'); - fetchMock.delete('*', { data: 'value' }); + it('remove vacancy with problem', () => { + fetchMock.delete('*', 404); + const modalPendaftaran = ReactTestUtils.renderIntoDocument( + <CancelModal id={4} />); + + const response3 = { student: { id: 1, name: 2 } }; + Storage.set('user-data', response3); + modalPendaftaran.removeVacancy(); + expect(modalPendaftaran.state.header).to.exist; + fetchMock.restore(); + }); + + it('confirm with problem', () => { + fetchMock.delete('*', 404); const modalPendaftaran = ReactTestUtils.renderIntoDocument( <CancelModal id={4} />); modalPendaftaran.confirm(); expect(modalPendaftaran.state.header).to.equal('Permintaan gagal'); + fetchMock.restore(); }); it('render next modal without problem', () => { - const fetchMock = require('fetch-mock'); - fetchMock.delete('*', { data: 'value' }); const modalPendaftaran = ReactTestUtils.renderIntoDocument( <CancelModal id={4} />); @@ -60,5 +71,4 @@ describe('CancelModal', () => { modalPendaftaran.forceUpdate() expect(modalPendaftaran).to.exist; }); - }); diff --git a/assets/js/__test__/components/CompanyRegister-test.jsx b/assets/js/__test__/components/CompanyRegister-test.jsx deleted file mode 100644 index 0a46e09ad33cceecbe43f784b756b7b033f7c34c..0000000000000000000000000000000000000000 --- a/assets/js/__test__/components/CompanyRegister-test.jsx +++ /dev/null @@ -1,13 +0,0 @@ -// /* eslint-disable no-unused-expressions */ -// import React from 'react'; -// import ReactTestUtils from 'react-addons-test-utils'; -// import Vacancy from '../components/ComponentRegister'; -// -// describe('Vacancy', () => { -// it('renders without problem', () => { -// let companyRegister = ReactTestUtils.renderIntoDocument( -// <CompanyRegister />); -// expect(companyRegister).to.exist; -// -// }); -// }); \ No newline at end of file diff --git a/assets/js/__test__/components/CompanyVacancy-test.js b/assets/js/__test__/components/CompanyVacancy-test.js new file mode 100644 index 0000000000000000000000000000000000000000..06d31e555077c3c79e79889bd9f26cbe0c7c1285 --- /dev/null +++ b/assets/js/__test__/components/CompanyVacancy-test.js @@ -0,0 +1,59 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import CompanyVacancy from '../../components/CompanyVacancy'; + +describe('CompanyVacancy', () => { + 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: true, + }; + + it('renders with logo without problem', () => { + const companyVacancy = ReactTestUtils.renderIntoDocument( + <CompanyVacancy data={response2} />); + expect(companyVacancy).to.exist; + }); + + it('renders without logo without problem', () => { + const companyVacancy = ReactTestUtils.renderIntoDocument( + <CompanyVacancy data={response} />); + expect(companyVacancy).to.exist; + }); + + it('loads when delete button clicked', () => { + const companyVacancy = ReactTestUtils.renderIntoDocument( + <CompanyVacancy data={response} />); + const button = ReactTestUtils.findRenderedDOMComponentWithTag(companyVacancy, 'Button'); + ReactTestUtils.Simulate.click(button); + expect(companyVacancy.state.deleteLoading).to.equal(true); + }); +}); diff --git a/assets/js/__test__/components/LoginForm-test.jsx b/assets/js/__test__/components/LoginForm-test.jsx index c8df38afdd83daa95e6e782a4c4c2bc7ed6b35cf..57f85c79d32080d90728fdc358e1058322c2c545 100644 --- a/assets/js/__test__/components/LoginForm-test.jsx +++ b/assets/js/__test__/components/LoginForm-test.jsx @@ -53,7 +53,7 @@ describe('LoginForm', () => { }); it('submit form without problem', () => { - fetchMock.post('*', {data: 'value'}); + fetchMock.post('*', { data: 'value' }); const formLogin = ReactTestUtils.renderIntoDocument( <LoginForm url="" />); @@ -62,5 +62,19 @@ describe('LoginForm', () => { const form = ReactTestUtils.findRenderedDOMComponentWithTag(formLogin, 'Form'); ReactTestUtils.Simulate.submit(form); + fetchMock.restore(); + }); + + it('submit form with problem', () => { + fetchMock.post('*', 404); + const formLogin = ReactTestUtils.renderIntoDocument( + <LoginForm url="" />); + + const submitButton = ReactTestUtils.findRenderedDOMComponentWithTag(formLogin, 'Button'); + ReactTestUtils.Simulate.click(submitButton); + + const form = ReactTestUtils.findRenderedDOMComponentWithTag(formLogin, 'Form'); + ReactTestUtils.Simulate.submit(form); + fetchMock.restore(); }); }); diff --git a/assets/js/__test__/components/Tabs-test.jsx b/assets/js/__test__/components/Tabs-test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3f809c710d246067e7b5d21554a518a13a048758 --- /dev/null +++ b/assets/js/__test__/components/Tabs-test.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactTestUtils from 'react-addons-test-utils'; +import Tabs from '../../components/Tabs'; + +describe('ApplyModal', () => { + it('renders without problem', () => { + const tabs = ReactTestUtils.renderIntoDocument( + <Tabs selected={1} children={[]} />, + ); + expect(tabs).to.exist; + }); + + it('renders handle click properly', () => { + const tabs = ReactTestUtils.renderIntoDocument( + <Tabs selected={1} children={[]} />, + ); + tabs.handleClick('stub', new Event('click')); + expect(tabs.state.selected).to.equal('stub'); + }); +}); diff --git a/assets/js/__test__/components/Vacancy-test.jsx b/assets/js/__test__/components/Vacancy-test.jsx index a98d55657dedf5404f493a9495e34b091aaa5369..8a08783eaa8520412da6aaaf335bef3bce6b24c3 100644 --- a/assets/js/__test__/components/Vacancy-test.jsx +++ b/assets/js/__test__/components/Vacancy-test.jsx @@ -8,11 +8,11 @@ describe('Vacancy', () => { const fetchMock = require('fetch-mock'); const response ={ close_time: '2019-03-28T05:55:42Z', - company: { + company: { address: 'kebayoran baru', - id: 1, - logo: null, - name: 'tutup lapak', + id: 1, + logo: null, + name: 'tutup lapak', }, created: '2017-03-28T07:05:47.128672Z', description: 'Lorem ipsum dolbh.', diff --git a/assets/js/__test__/components/VacancyList-test.jsx b/assets/js/__test__/components/VacancyList-test.jsx index f95f7ab907cd906a90ebe14a5aeb1bbd1265ff3e..3ecfb60898af5a895820f860fcef693582835091 100644 --- a/assets/js/__test__/components/VacancyList-test.jsx +++ b/assets/js/__test__/components/VacancyList-test.jsx @@ -1,11 +1,10 @@ /* eslint-disable no-unused-expressions */ import React from 'react'; import ReactTestUtils from 'react-addons-test-utils'; +import fetchMock from 'fetch-mock'; import VacancyList from '../../components/VacancyList'; -import Server from '../../lib/Server'; describe('VacancyList', () => { - const fetchMock = require('fetch-mock'); const response = [{ close_time: '2019-03-28T05:55:42Z', company: { @@ -22,39 +21,93 @@ describe('VacancyList', () => { updated: '2017-03-28T07:34:13.122093Z', verified: true, }]; - const response2 = { hello: 'not-world' }; it('renders without problem', () => { fetchMock.get('*', response); const vacancyList = ReactTestUtils.renderIntoDocument( - <VacancyList studentId={1} url="test" />); + <VacancyList userId={1} url="test" />); expect(vacancyList).to.exist; + fetchMock.restore(); + }); + + it('renders without problem for company', () => { + fetchMock.get('*', response); + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList type="company" userId={1} url="test" />); + vacancyList.state.vacancies = response; + expect(vacancyList.generateVacancies()).to.exist; + fetchMock.restore(); }); it('update bookmarks without problem', () => { fetchMock.get('*', response); const vacancyList = ReactTestUtils.renderIntoDocument( - <VacancyList studentId={1} url="test" />); + <VacancyList userId={1} url="test" />); vacancyList.updateBookmarkList(); expect(JSON.stringify(vacancyList.state.bookmarkList)).to.be.defined; + fetchMock.restore(); }); it('renders marked bookmarked vacancies without problem', () => { fetchMock.get('*', response); const vacancyList = ReactTestUtils.renderIntoDocument( - <VacancyList studentId={1} url="test" />); + <VacancyList userId={1} url="test" />); + vacancyList.state.vacancies = response; + vacancyList.state.bookmarkList = [{ id: 5 }, { id: 3 }, { id: 1 }]; + expect(vacancyList.generateVacancies()).to.exist; + fetchMock.restore(); + }); + + it('renders not marked vacancies without problem', () => { + fetchMock.get('*', response); + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList userId={1} url="test" />); vacancyList.state.vacancies = response; - vacancyList.state.bookmarkList = [{id: 5}, {id: 3}]; + vacancyList.state.bookmarkList = [{ id: 6 }, { id: 4 }, { id: 2 }]; expect(vacancyList.generateVacancies()).to.exist; + fetchMock.restore(); }); it('success calling API', () => { fetchMock.get('*', response); const vacancyList = ReactTestUtils.renderIntoDocument( - <VacancyList studentId={1} url="test" />); + <VacancyList userId={1} url="test" />); vacancyList.state.vacancies = response; expect(JSON.stringify(vacancyList.state.vacancies)).to.equal(JSON.stringify(response)); - expect(vacancyList.generateVacancies()).to.exist; + fetchMock.restore(); + }); + + it('success delete vacancy', (done) => { + fetchMock.delete('*', response); + fetchMock.get('*', response); + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList userId={1} url="test" deleteCallback={() => {}} />); + vacancyList.state.vacancies = response; + vacancyList.deleteVacancy(1).then(() => { + expect(JSON.stringify(vacancyList.state.vacancies)).to.equal(JSON.stringify(response)); + fetchMock.restore(); + done(); + }, () => { + fetchMock.restore(); + done(); + }); + }); + + it('fails delete vacancy', (done) => { + fetchMock.delete('*', 404); + fetchMock.get('*', response); + const vacancyList = ReactTestUtils.renderIntoDocument( + <VacancyList userId={1} url="test" deleteCallback={() => {}} /> + ); + vacancyList.state.vacancies = response; + vacancyList.deleteVacancy(1).then(() => { + fetchMock.restore(); + done(); + }, () => { + expect(JSON.stringify(vacancyList.state.vacancies)).to.equal(JSON.stringify(response)); + fetchMock.restore(); + done(); }); }); +}); diff --git a/assets/js/__test__/lib/Dumper-test.jsx b/assets/js/__test__/lib/Dumper-test.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fa6c2154b6fe2e0e22f5071e5c7ca1e54d4b77db --- /dev/null +++ b/assets/js/__test__/lib/Dumper-test.jsx @@ -0,0 +1,17 @@ +/* eslint-disable no-unused-expressions */ +import Dumper from './../../lib/Dumper'; + +describe('Dumper', () => { + it('Dumper dump successfully', () => { + const val = Dumper.dump({ + hue: ['tes1', 'tes2'], + hui: { huo: '123' }, + }); + expect(val).to.be.exist; + }); + + it('Dumper dump correctly', () => { + const val = Dumper.dump({ hue: [1, 2, 3] }); + expect(val).to.equal('hue : [ 1,2,3 ]'); + }) +}); diff --git a/assets/js/components/ApplyModal.jsx b/assets/js/components/ApplyModal.jsx index 5d00fb590806dea85ca82421586de97cf50e8dd2..4a06139d46404b9c0e9c1bc7cb63aea45271a48c 100644 --- a/assets/js/components/ApplyModal.jsx +++ b/assets/js/components/ApplyModal.jsx @@ -35,7 +35,7 @@ export default class ApplyModal extends React.Component { render = () => ( <Modal - trigger={<Button onClick={this.handleOpen} floated="right">{this.props.buttonTitle}</Button>} + trigger={<Button primary onClick={this.handleOpen} floated="right">{this.props.buttonTitle}</Button>} closeIcon="close" open={this.state.modalOpen} onClose={this.handleClose} diff --git a/assets/js/components/CancelModal.jsx b/assets/js/components/CancelModal.jsx index 723bc3bcc76cc7e68a39e61196b66d03527adca4..662847cbbf50a9f43d672032fa56e81343a88d8c 100644 --- a/assets/js/components/CancelModal.jsx +++ b/assets/js/components/CancelModal.jsx @@ -52,7 +52,6 @@ export default class CancelModal extends React.Component { header: 'Permintaan gagal', content: this.batalFailed, }); - } open = () => this.setState({ modalOpen: true }); @@ -80,4 +79,4 @@ export default class CancelModal extends React.Component { </Modal.Actions> </Modal> ); -} \ No newline at end of file +} diff --git a/assets/js/components/RegisterModal.jsx b/assets/js/components/CompanyRegisterModal.jsx similarity index 90% rename from assets/js/components/RegisterModal.jsx rename to assets/js/components/CompanyRegisterModal.jsx index c32a10ba3c62b6d5305e5cb24917fe6956968ff8..1197a47fe8bb3c92176cb175aaccec8eb22b0617 100644 --- a/assets/js/components/RegisterModal.jsx +++ b/assets/js/components/CompanyRegisterModal.jsx @@ -5,7 +5,7 @@ import ModalAlert from './../components/ModalAlert'; import Server from './../lib/Server'; import Storage from './../lib/Storage'; -export default class RegisterModal extends React.Component { +export default class CompanyRegisterModal extends React.Component { constructor(props) { super(props); @@ -44,7 +44,7 @@ export default class RegisterModal extends React.Component { }; render = () => ( - <Modal trigger={<Button primary floated="right">Register Here!</Button>} closeIcon="close"> + <Modal trigger={<Button primary floated="right">Register</Button>} closeIcon="close"> <Header icon="archive" content="Register for More Benefits" /> <Modal.Content> <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> @@ -53,7 +53,7 @@ export default class RegisterModal extends React.Component { <Icon name="signup" circular /> <Header.Content> Register - </Header.Content> + </Header.Content> </Header> <Form.Field required> <label htmlFor="email">Email</label> @@ -91,16 +91,18 @@ export default class RegisterModal extends React.Component { </Form.Field> <Form.Field required> <label htmlFor="description">Deskripsi</label> - <TextArea onChange={this.handleChange} placeholder="Tell us more" name="description" required /> + <TextArea onChange={this.handleChange} placeholder="Tell us more" name="description" autoHeight required /> </Form.Field> <Form.Field required> <label htmlFor="address">Alamat</label> <Input onChange={this.handleChange} placeholder="Alamat" name="address" required /> </Form.Field> - <Button type="submit" floated="right" color="blue">Submit</Button> + </Form> </Modal.Content> - <Modal.Actions /> + <Modal.Actions> + <Button type="submit" color="blue"> <Icon name="checkmark" />Submit</Button> + </Modal.Actions> </Modal> ) } diff --git a/assets/js/components/CompanyVacancy.jsx b/assets/js/components/CompanyVacancy.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8ff71d10692b46767974fb15ae97d2c61abdb424 --- /dev/null +++ b/assets/js/components/CompanyVacancy.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import moment from 'moment'; +import { Button, Icon, Item, Grid } from 'semantic-ui-react'; +import { Link } from 'react-router'; + +const defaultImage = 'http://semantic-ui.com/images/wireframe/image.png'; + +export default class CompanyVacancy extends React.Component { + static propTypes = { + data: React.PropTypes.object.isRequired, + deleteCallback: React.PropTypes.func, + }; + + static defaultProps = { deleteCallback: () => {} }; + + constructor(props) { + super(props); + moment.locale('id'); + this.state = { deleteLoading: false }; + } + + getLink = `/buat-lowongan/${this.props.data.id}`; + + render() { + return ( + <Item className="applicantItems"> + <Item.Image src={this.props.data.company.logo ? this.props.data.company.logo : defaultImage} size="small" /> + <Item.Content> + <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()} + </Grid.Column> + <Grid.Column floated="right"> + {this.props.data.verified ? + (<h4> <Icon name="checkmark box" size="large" color="green" /> Terverifikasi </h4>) : + (<h4> <Icon name="remove circle" size="large" color="red" /> Belum Terverifikasi </h4>)} + <Button color="blue" floated="right" as={Link} to={this.getLink}> + Ubah <Icon name="right chevron" /> + </Button> + <Button loading={this.state.deleteLoading} color="red" floated="right" onClick={() => { this.setState({ deleteLoading: true }); this.props.deleteCallback(); }} > + Hapus <Icon name="delete" /> + </Button> + </Grid.Column> + </Grid.Row> + </Item.Content> + </Item> + ); + } +} diff --git a/assets/js/components/LoginForm.jsx b/assets/js/components/LoginForm.jsx index dcb2f06889a662e7ea9abd097b6112f9e753e018..eb120ba307153ce00471fc2616cbaf2b42f88e73 100644 --- a/assets/js/components/LoginForm.jsx +++ b/assets/js/components/LoginForm.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Form, Input, Button, Message, Image, Card } from 'semantic-ui-react'; +import { Form, Input, Button, Message, Image, Card, Header, Segment } from 'semantic-ui-react'; import { browserHistory } from 'react-router'; import Server from '../lib/Server'; import Storage from '../lib/Storage'; @@ -22,7 +22,7 @@ export default class LoginForm extends React.Component { constructor(props) { super(props); /* istanbul ignore next */ - this.state = { username: '', password: '', errorFlag: false }; + this.state = { username: '', password: '', errorFlag: false, loading: false }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } @@ -38,12 +38,12 @@ export default class LoginForm extends React.Component { username: this.state.username, password: this.state.password, }; - + this.setState({ loading: true }); Server.post('/login/', data).then((response) => { Storage.set('user-data', response); browserHistory.push('/home'); }, () => { - this.setState({ errorFlag: true }); + this.setState({ errorFlag: true, loading: false }); }); } @@ -51,35 +51,33 @@ export default class LoginForm extends React.Component { <div className="formLogin" > - - <Form onSubmit={e => this.handleSubmit(e)} error={this.state.errorFlag}> - <div className="formHeader"> + <Segment.Group> + <Segment> <Image src={`./assets/img/${this.props.imgSrc}`} size={this.props.imgSize} verticalAlign="middle" /> <span>{ this.props.header }</span> - </div> - <Form.Group widths="equal"> - <Form.Field> - <label htmlFor="id"> Username </label> - <Input type="text" id="username" icon="user" iconPosition="left" placeholder="username" onChange={e => this.handleChange(e, 'username')} required /> - </Form.Field> - </Form.Group> - - <Form.Group widths="equal"> - <Form.Field> - <label htmlFor="password"> Password </label> - <Input type="password" id="password" icon="key" iconPosition="left" placeholder="password" onChange={e => this.handleChange(e, 'password')} required /> - </Form.Field> - </Form.Group> - - <Button type="submit" fluid color="blue">Login</Button> - <Message - error - content="Login gagal: username atau password salah." - /> - - </Form> - - + </Segment> + + <Segment inverted className="header" > + <Form loading={this.state.loading} onSubmit={e => this.handleSubmit(e)} error={this.state.errorFlag}> + <Form.Group widths="equal"> + <Form.Field> + <label htmlFor="id"> Username </label> + <Input type="text" id="username" icon="user" iconPosition="left" placeholder="username" onChange={e => this.handleChange(e, 'username')} required /> + </Form.Field> + </Form.Group> + <Form.Group widths="equal"> + <Form.Field> + <label htmlFor="password"> Password </label> + <Input type="password" id="password" icon="key" iconPosition="left" placeholder="password" onChange={e => this.handleChange(e, 'password')} required /> + </Form.Field> + </Form.Group> + <Button type="submit" fluid color="blue">Login</Button> + <Message + error + content="Login gagal: username atau password salah." + /> + </Form> + </Segment> + </Segment.Group> </div> - ) } diff --git a/assets/js/components/Lowongan.jsx b/assets/js/components/Lowongan.jsx deleted file mode 100644 index 0d9be60fd90cc899a479667da3a6663f93695bda..0000000000000000000000000000000000000000 --- a/assets/js/components/Lowongan.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Button, Image as ImageComponent, Item, Rating, Icon } from 'semantic-ui-react' -import ModalPendaftaran from './ApplyModal'; - - - const paragraph = <ImageComponent src="http://semantic-ui.com/images/wireframe/short-paragraph.png" />; -const image = <Item.Image size="small" src="http://semantic-ui.com/images/wireframe/image.png" />; -export default class Lowongan extends React.Component { - static propTypes = { - data: React.PropTypes.object.isRequired, - }; - - render() { - return ( - - <Item > - {image} - <Item.Content verticalAlign="middle"> - <Item.Header> - {this.props.header} - </Item.Header> - <Item.Description>{this.props.content}</Item.Description> - <Item.Content> - - <h3>{ this.props.data.data1 }</h3> - <h4>PT. Koding Kuat </h4> - <h5> JL.Kali deres utara no.1 Jakarta Barat, DKI Jakarta</h5> - - <ApplicancyModal id={1} data={ { header: 'Deskripsi Lowongan', description: 'Lorem ipsum dolor sit amet'} } buttonTitle="Daftar" /> - - </Item.Content> - </Item.Content> - </Item> - - ); - } -} \ No newline at end of file diff --git a/assets/js/components/ModalAlert.jsx b/assets/js/components/ModalAlert.jsx index de5e94f2950c526ebe1a1c02469dac30b6b3e1c6..10b7e0e6f0e709e271277fb093a2042001658355 100644 --- a/assets/js/components/ModalAlert.jsx +++ b/assets/js/components/ModalAlert.jsx @@ -31,17 +31,20 @@ export default class ModalAlert extends React.Component { this.setState({ open: false }); }; - render = () => ( - <Modal open={this.state.open} basic size="small"> - <Header icon="warning sign" content={this.state.header} /> - <Modal.Content> - <p>{this.state.content}</p> - </Modal.Content> - <Modal.Actions> - <Button color="green" inverted onClick={this.close}> - <Icon name="checkmark" /> OK - </Button> - </Modal.Actions> - </Modal> - ); + render = () => { + const style = { whiteSpace: 'pre-wrap' }; + return ( + <Modal open={this.state.open} basic size="small"> + <Header icon="warning sign" content={this.state.header} /> + <Modal.Content> + <p style={style}>{this.state.content}</p> + </Modal.Content> + <Modal.Actions> + <Button color="green" inverted onClick={this.close}> + <Icon name="checkmark" /> OK + </Button> + </Modal.Actions> + </Modal> + ); + }; } diff --git a/assets/js/components/Tabs.jsx b/assets/js/components/Tabs.jsx index 601d8f45e2bf2fd07e2b0aa7f3468b227bb1b464..aa6f75109b1c24f908a9a88043653e465dd60a37 100644 --- a/assets/js/components/Tabs.jsx +++ b/assets/js/components/Tabs.jsx @@ -11,10 +11,6 @@ export default class Tabs extends React.Component { ]).isRequired, }; - static defaultProps = () => ({ - selected: 0, - }); - constructor(props) { super(props); this.state = { diff --git a/assets/js/components/TopMenu.jsx b/assets/js/components/TopMenu.jsx index ad5b6bed5151a807eaffa37acabe83e5511864c4..43319101a03cefeb8bdcadaf9be8db0b7cd4a573 100644 --- a/assets/js/components/TopMenu.jsx +++ b/assets/js/components/TopMenu.jsx @@ -7,15 +7,15 @@ import Storage from '../lib/Storage'; export default class TopMenu extends React.Component { - state = { activeItem: 'home' }; - handleItemClick = (e, { name }) => this.setState({ activeItem: name }); - constructor(props) { super(props); /* istanbul ignore next */ + this.state = { activeItem: 'home' }; this.logout = this.logout.bind(this); } + handleItemClick = (e, { name }) => this.setState({ activeItem: name }); + logout = () => { Server.get('/api-auth/logout/?next=/', true).then(() => { Storage.clear(); diff --git a/assets/js/components/Vacancy.jsx b/assets/js/components/Vacancy.jsx index 8b98a8d21715b7560cad67a594d880c517b77d4b..65eff411bd82965f92536858560a83590cdaf9cf 100644 --- a/assets/js/components/Vacancy.jsx +++ b/assets/js/components/Vacancy.jsx @@ -49,7 +49,7 @@ export default class Vacancy extends React.Component { render() { return ( - <Item > + <Item className="applicantItems"> <Item.Image size="small" src={this.props.data.company.logo ? this.props.data.company.logo : defaultImage} /> <Item.Content verticalAlign="middle"> <Item.Extra> diff --git a/assets/js/components/VacancyList.jsx b/assets/js/components/VacancyList.jsx index 5188044ef382767a33cc795eb9ae9dd4963887e2..e3603949dae305e277197f0760dc9560c4f2c1b6 100644 --- a/assets/js/components/VacancyList.jsx +++ b/assets/js/components/VacancyList.jsx @@ -1,31 +1,40 @@ import React from 'react'; -import { Item } from 'semantic-ui-react'; +import { Item, Button, Grid, Segment } from 'semantic-ui-react'; +import { Link } from 'react-router'; import Vacancy from './Vacancy'; +import CompanyVacancy from './CompanyVacancy'; import Server from '../lib/Server'; +import ModalAlert from '../components/ModalAlert'; + export default class VacancyList extends React.Component { static propTypes = { url: React.PropTypes.string.isRequired, - studentId: React.PropTypes.number.isRequired, + userId: React.PropTypes.number.isRequired, status: React.PropTypes.string, + type: React.PropTypes.string, }; static defaultProps = { status: 'Daftar', + type: 'student', }; constructor(props) { super(props); /* istanbul ignore next */ - this.updateBookmarkList(); + if (this.props.type === 'student') { + this.updateBookmarkList(); + } + this.state = { vacancies: [], bookmarkList: [] }; Server.get(this.props.url, false).then((data) => { this.setState({ vacancies: data }); }); - this.state = { vacancies: [], bookmarkList: [] }; this.updateBookmarkList = this.updateBookmarkList.bind(this); this.generateVacancies = this.generateVacancies.bind(this); this.checkBookmark = this.checkBookmark.bind(this); + this.companyHeader = this.companyHeader.bind(this); } checkBookmark(id) { @@ -36,26 +45,64 @@ export default class VacancyList extends React.Component { } updateBookmarkList() { - Server.get(`/students/${this.props.studentId}/bookmarked-vacancies/`, false).then((data) => { + Server.get(`/students/${this.props.userId}/bookmarked-vacancies/`, false).then((data) => { this.setState({ bookmarkList: data }); }); } + deleteVacancy = id => Server.delete(`/vacancies/${id}/`, this.state).then(() => { + this.modalAlert.open('Hapus Lowongan', 'Lowongan berhasil dihapus'); + const newVacancies = []; + this.state.vacancies.map(vacancy => vacancy.id !== id && newVacancies.push(vacancy)); + this.setState({ vacancies: newVacancies }); + }, error => error.then((r) => { + this.modalAlert.open('Gagal Menghapus Lowongan', r.error); + })); + generateVacancies() { - return this.state.vacancies.map(vacancy => - <Vacancy - key={vacancy.id} - status={this.props.status} - bookmarked={this.checkBookmark(vacancy.id)} - data={vacancy} - />, + if (this.props.type === 'student') { + return this.state.vacancies.map(vacancy => + ( + <Vacancy + key={vacancy.id} + status={this.props.status} + bookmarked={this.checkBookmark(vacancy.id)} + data={vacancy} + /> + ), + ); + } + + return this.state.vacancies.map(vacancy => (<CompanyVacancy + key={vacancy.id} + data={vacancy} + deleteCallback={() => this.deleteVacancy(vacancy.id)} + />), ); } + companyHeader() { + if (this.props.type === 'company') { + return ( + <Grid textAlign="center"> + <Button size="tiny" icon="eye" labelPosition="left" color="facebook" content="Lihat Semua Pendaftar" /> + <Button size="tiny" as={Link} to="/buat-lowongan" icon="add" labelPosition="left" content="Tambah Lowongan Baru" color="teal" /> + </Grid> + ); + } + + return ''; + } render = () => ( - <Item.Group relaxed> - { this.generateVacancies() } - </Item.Group> + <Segment> + <ModalAlert ref={(modal) => { this.modalAlert = modal; }} /> + <Grid container columns="eleven" doubling> + { this.companyHeader() } + <Item.Group relaxed style={{ width: '100%' }}> + { this.generateVacancies() } + </Item.Group> + </Grid> + </Segment> ); } diff --git a/assets/js/index.jsx b/assets/js/index.jsx index d54b82b5dcd637ce293de64c41179424e2783f93..54bb52acb934100274f38bda17e406d8f8d70ce5 100644 --- a/assets/js/index.jsx +++ b/assets/js/index.jsx @@ -1,17 +1,32 @@ -/* eslint-disable no-multi-comp */ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, browserHistory, Redirect } from 'react-router'; -import { Segment } from 'semantic-ui-react'; import Dashboard from './Dashboard'; import Login from './Login'; import VacancyPage from './VacancyPage'; +import CreateVacancy from './CreateVacancy'; import Server from './lib/Server'; import Storage from './lib/Storage'; - +import ApplicantPage from './ApplicantPage'; export default class App extends React.Component { + static getRole() { + if (Server.isLoggedIn()) { + if (Storage.get('user-data').is_staff) { + return 'admin'; + } else if (Storage.get('user-data').supervisor) { + return 'supervisor'; + } else if (Storage.get('user-data').student) { + return 'student'; + } else if (Storage.get('user-data').company) { + return 'company'; + } + return 'error'; + } + return 'not-logged-in'; + } + constructor(props) { super(props); /* istanbul ignore next */ @@ -19,41 +34,26 @@ export default class App extends React.Component { this.handleHome = this.handleHome.bind(this); } - authorization = WrappedComponent => - allowedRoles => ( + authorization = allowedRoles => + WrappedComponent => ( + /* eslint-disable no-multi-comp */ class WithAuthorization extends React.Component { constructor(props) { super(props); this.state = { user: { - role: WithAuthorization.getRole(), + role: App.getRole(), }, }; } - static getRole() { - if (Server.isLoggedIn()) { - if (Storage.get('user-data').is_staff) { - return 'admin'; - } else if (Storage.get('user-data').supervisor) { - return 'supervisor'; - } else if (Storage.get('user-data').student) { - return 'student'; - } else if (Storage.get('user-data').company) { - return 'company'; - } - return 'error'; - } - return 'not-logged-in'; - } - render() { const { role } = this.state.user; if (allowedRoles.includes(role)) { return <WrappedComponent {...this.props} />; } - return browserHistory.goBack; + return <div> { browserHistory.push('/home') } </div>; } }); @@ -64,26 +64,31 @@ export default class App extends React.Component { handleHome= (nextState, replace, cb) => { if (Server.isLoggedIn()) { - Storage.getUserData().then((data) => { - const next = data.student ? '/lowongan' : '/profile'; - replace({ pathname: next }); cb(); + Storage.getUserData().then(() => { + if (App.getRole() === 'student') { + replace({ pathname: '/lowongan' }); cb(); + } else if (App.getRole() === 'company') { + replace({ pathname: '/pelamar' }); cb(); + } }); - } else { - replace({ pathname: '/login' }); } + replace({ pathname: '/login' }); cb(); }; render() { - const student = this.authorization(['admin', 'student']); - const supervisor = this.authorization(['admin', 'supervisor']); + // const student = this.authorization(['admin', 'student']); + // const supervisor = this.authorization(['admin', 'supervisor']); const company = this.authorization(['admin', 'company']); const commonUser = this.authorization(['admin', 'student', 'company']); return ( <Router history={browserHistory}> - <Route path="/login" component={Login}/> + <Route path="/login" component={Login} /> <Route component={Dashboard} onEnter={this.handleAuth}> <Route path="/lowongan" component={commonUser(VacancyPage)} /> + <Route path="/buat-lowongan" component={company(CreateVacancy)} /> + <Route path="/buat-lowongan/:id" component={company(CreateVacancy)} /> + <Route path="/pelamar" component={company(ApplicantPage)} /> </Route> <Route path="/home" onEnter={this.handleHome} /> <Redirect from="*" to="/home" /> diff --git a/assets/js/lib/Dumper.jsx b/assets/js/lib/Dumper.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3a392116f55113c49e98ddc370757225c91e1a90 --- /dev/null +++ b/assets/js/lib/Dumper.jsx @@ -0,0 +1,26 @@ +/* eslint-disable no-console no-restricted-syntax no-param-reassign */ + +export default class Dumper { + static dump(obj, indent) { + let result = ''; + if (indent == null) indent = ''; + + for (const property in obj) { + let value = obj[property]; + + if (typeof value === 'string') { + value = `${value}`; + } else if (typeof value === 'object') { + if (value instanceof Array) { + // Just let JS convert the Array to a string! + value = `[ ${value} ]`; + } else { + const od = this.dump(value, `${indent} `); + value = `\n${od}`; + } + } + result += `${indent}${property} : ${value},\n`; + } + return result.replace(/,\n$/, ''); + } +} diff --git a/assets/js/lib/Server.jsx b/assets/js/lib/Server.jsx index bf53cf6f7b37cb2fff811fd150dced370fe4df0b..030bba1c45f3698def46a04d7d0534b882cab53f 100644 --- a/assets/js/lib/Server.jsx +++ b/assets/js/lib/Server.jsx @@ -76,7 +76,7 @@ export default class Server { }); } - static get(path, useCache = true) { + static get(path, useCache = false) { return (useCache && Storage.get(path)) ? Promise.resolve(Storage.get(path)) : this.sendRequest(path, 'GET', null, useCache); } diff --git a/core/serializers/vacancies.py b/core/serializers/vacancies.py index 090d4b7aa01ee7f5e3735829e9cf9b9b03c5cabd..3926965db88f6265a4b99def6c14d3da21c3026d 100644 --- a/core/serializers/vacancies.py +++ b/core/serializers/vacancies.py @@ -1,10 +1,14 @@ from rest_framework import serializers -from core.serializers.accounts import CompanySerializer, StudentSerializer + +from core.models import Company from core.models.vacancies import Vacancy, Application +from core.serializers.accounts import StudentSerializer class VacancySerializer(serializers.ModelSerializer): - company = CompanySerializer() + company = serializers.PrimaryKeyRelatedField( + queryset=Company.objects.all() + ) class Meta: model = Vacancy diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 4c14575b552233b6c12fefd25d56f13c037f2c8d..b15d9a2c9065ff9db99bf69bac81afd41914bbbc 100755 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -8,6 +8,7 @@ <title>Yuk Cari Tempat Kape :)</title> <link rel="stylesheet" href="{% static 'css/custom.css' %}"/> <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" /> </head> diff --git a/core/tests/__init__.py b/core/tests/__init__.py index f7f9dcaac3cb74a621959c4707fda71afbfb3cbe..76d673e4b0147d067a601ad568adbcc76c97c056 100755 --- a/core/tests/__init__.py +++ b/core/tests/__init__.py @@ -1,3 +1,3 @@ # __init__.py from core.tests.test_accounts import LoginTests, RegisterTests -from core.tests.test_vacancies import ApplicationTests, BookmarkApplicationTests +from core.tests.test_vacancies import ApplicationTests, BookmarkApplicationTests, CompanyListsTests diff --git a/core/tests/test_studentViewSet.py b/core/tests/test_studentViewSet.py deleted file mode 100644 index efd3f41311b54636109a6e58bbb8dfd33eb90439..0000000000000000000000000000000000000000 --- a/core/tests/test_studentViewSet.py +++ /dev/null @@ -1,18 +0,0 @@ -# from unittest import TestCase -# -# from django.urls import reverse -# -# -# class TestStudentViewSet(TestCase): -# # def setUp(self): -# # #c = Client() -# # Student.objects.create(user = User.objects.create(username = "farhan"), npm = "1406572321") -# -# #def test_bookmark_vacancies(self): -# # url = reverse('bookmarked-vacancies') -# # data = {'company_id': 1} -# # response = self.client.post(url, data, format='json') -# # self.fail() -# # -# # def test_remove_vacancies(self): -# # self.fail() diff --git a/core/tests/test_userViewSet.py b/core/tests/test_userViewSet.py index 5a7822067e9c40c916127a2b93c66943ddd0ab91..ad2179c83312890d5c13569ef9c0f1fb6573ba44 100644 --- a/core/tests/test_userViewSet.py +++ b/core/tests/test_userViewSet.py @@ -1,6 +1,46 @@ -# from unittest import TestCase -# -# -# class TestUserViewSet(TestCase): -# def test_me(self): -# self.fail() +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APITestCase + + +class TestUserViewSet(APITestCase): + def test_me_success(self): + superuser = User.objects.create_superuser('dummy.user', 'dummy.user@user.com', 'lalala123') + self.client.force_authenticate(user=superuser) + + url = '/api/users/me/' + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_users_fail(self): + url = '/api/users/' + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.put(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.patch(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_user_students_success(self): + superuser = User.objects.create_superuser('dummy.user', 'dummy.user@user.com', 'lalala123') + self.client.force_authenticate(user=superuser) + + url = '/api/students/' + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_user_students_fail(self): + url = '/api/students/' + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.post(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.put(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.patch(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/core/tests/test_vacancies.py b/core/tests/test_vacancies.py index 518a075737076084da1b7dc2ac77346445822de7..bb9829681f771a97e7d83a4485e0c39bbdcf2f43 100644 --- a/core/tests/test_vacancies.py +++ b/core/tests/test_vacancies.py @@ -1,13 +1,15 @@ from datetime import datetime + import requests_mock +from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase -from django.contrib.auth.models import User + from core.models.accounts import Company from core.models.vacancies import Vacancy -class ApplicationTests(APITestCase): +class ApplicationTests(APITestCase): @requests_mock.Mocker() def test_application_list(self, m): m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ @@ -24,12 +26,11 @@ class ApplicationTests(APITestCase): 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) + '/applications/' + url = '/api/students/' + str(student_id) + '/applied-vacancies/' 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.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ @@ -51,16 +52,16 @@ class ApplicationTests(APITestCase): new_company = Company.objects.create(user=new_user, description="lalala",verified=True,logo=None,address=None) new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0), description="lalala", close_time=datetime.today()) - url = '/api/students/' + str(student_id) + '/applications/' + url = '/api/students/' + str(student_id) + '/applied-vacancies/' response = self.client.post(url, {'vacancy_id' : new_vacancy.pk, 'cover_letter' : 'this is a cover letter.'}, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - url = '/api/students/' + str(student_id) + '/applications/' + str(new_vacancy.pk) + '/' + url = '/api/students/' + str(student_id) + '/applied-vacancies/' + str(new_vacancy.pk) + '/' response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_200_OK) -class BookmarkApplicationTests(APITestCase): +class BookmarkApplicationTests(APITestCase): @requests_mock.Mocker() def test_application_list(self, m): m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ @@ -82,7 +83,6 @@ class BookmarkApplicationTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) - @requests_mock.Mocker() def test_application_create_and_delete(self, m): m.post('https://api.cs.ui.ac.id/authentication/ldap/v2/', json={ @@ -100,7 +100,7 @@ class BookmarkApplicationTests(APITestCase): format='json') student_id = response.data.get('student').get('id') - new_user = User.objects.create_user('dummy.company', 'dummy.company@company.com', 'lalala123') + new_user = User.objects.create_user('dummy.company2', 'dummy.compan2y@company.com', 'lalala123') new_company = Company.objects.create(user=new_user, description="lalala",verified=True,logo=None,address=None) new_vacancy = Vacancy.objects.create(company=new_company, verified=True, open_time=datetime.fromtimestamp(0), description="lalala", close_time=datetime.today()) @@ -111,3 +111,45 @@ class BookmarkApplicationTests(APITestCase): url = '/api/students/' + str(student_id) + '/bookmarked-vacancies/' + str(new_vacancy.pk) + '/' response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_200_OK) + +class VacancyTest(APITestCase): + def test_verified_vacancy_list(self): + superuser = User.objects.create_superuser('dummy.company', 'dummy.company@company.com', 'lalala123') + self.client.force_authenticate(user=superuser) + + url = '/api/vacancies/' + response = self.client.get(url, format='json') + 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) + + url = '/api/vacancies/?verified=false' + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_fail_on_unverified_user_vacancy_list(self): + url = '/api/vacancies/' + response = self.client.post(url, format='json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class CompanyListsTests(APITestCase): + + def test_company_vacancy_list_exist(self): + new_user = User.objects.create_user('dummy.company3', 'dummy.company3@company.com', 'lalala123') + new_company = Company.objects.create(user=new_user, description="lalala", verified=True, logo=None, address=None) + + url = '/api/companies/' + str(new_company.pk) + '/vacancies' + response = self.client.post(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_company_application_list_exist(self): + new_user = User.objects.create_user('dummy.company4', 'dummy.company4@company.com', 'lalala123') + new_company = Company.objects.create(user=new_user, description="lalala", verified=True, logo=None, + address=None) + + url = '/api/companies/' + str(new_company.pk) + '/applications' + response = self.client.post(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/core/views/vacancies.py b/core/views/vacancies.py index 02f6ff68f5a0e690fc29c7747a0d40de300ed24d..ff761057fe3bf58ac38a1d22db1663739f6c5ee7 100644 --- a/core/views/vacancies.py +++ b/core/views/vacancies.py @@ -5,21 +5,29 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from core.lib.permissions import IsAdminOrStudent, IsAdminOrCompany -from core.models import Student +from core.models import Student, Company from core.models.vacancies import Vacancy, Application from core.serializers.vacancies import VacancySerializer, ApplicationSerializer class VacancyViewSet(viewsets.ModelViewSet): - queryset = Vacancy.objects.all().filter(verified=True) + queryset = Vacancy.objects.all() serializer_class = VacancySerializer permission_classes = [IsAdminOrCompany] def get_permissions(self): - if self.action in ["get", "list"]: + if self.action in ["retrieve", "list"]: return [IsAuthenticated()] 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" + if verified.lower() in ("yes", "true", "t", "1"): + vacancies = vacancies.filter(verified=True) + + return Response(VacancySerializer(vacancies, many=True, context={'request': request}).data) + class ApplicationViewSet(viewsets.GenericViewSet): serializer_class = ApplicationSerializer @@ -66,6 +74,33 @@ class ApplicationViewSet(viewsets.GenericViewSet): return Response(ApplicationSerializer(application, context={'request': request}).data) +class CompanyApplicationViewSet(viewsets.GenericViewSet): + queryset = Application.objects.all() + + def list(self, request, company_id): + """ + Get list of company {company_id}'s applications + --- + """ + company = get_object_or_404(Company.objects.all(), pk=company_id) + vacancies = Vacancy.objects.filter(company = company) + applications = Application.objects.filter(vacancy__in = vacancies) + return Response(ApplicationSerializer(applications, many=True, context={'request': request}).data) + + +class CompanyVacanciesViewSet(viewsets.GenericViewSet): + queryset = Vacancy. objects.all() + + def list(self, request, company_id): + """ + Get list of company {company_id}'s vacancies + --- + """ + company = get_object_or_404(Company.objects.all().order_by('-updated'), pk=company_id) + vacancies = Vacancy.objects.filter(company=company) + return Response(VacancySerializer(vacancies, many=True, context={'request': request}).data) + + class BookmarkedVacancyByStudentViewSet(viewsets.GenericViewSet): serializer_class = VacancySerializer permission_classes = [IsAdminOrStudent] diff --git a/kape/urls.py b/kape/urls.py index a6e641fd1fb1fd11724512df67b91ee767d9dd91..2606f3deb7c9948ec4482f6339f85561fef55234 100755 --- a/kape/urls.py +++ b/kape/urls.py @@ -23,7 +23,7 @@ from rest_framework_swagger.views import get_swagger_view from core import apps from core.views.accounts import StudentViewSet, CompanyViewSet, SupervisorViewSet, UserViewSet, LoginViewSet, CompanyRegisterViewSet -from core.views.vacancies import VacancyViewSet, BookmarkedVacancyByStudentViewSet, ApplicationViewSet +from core.views.vacancies import VacancyViewSet, BookmarkedVacancyByStudentViewSet, ApplicationViewSet, CompanyApplicationViewSet, CompanyVacanciesViewSet schema_view = get_swagger_view() router = routers.DefaultRouter() @@ -38,7 +38,10 @@ router.register(r'students/(?P<student_id>\d+)/bookmarked-vacancies', Bookmarked base_name='bookmarked-vacancy-list') router.register(r'students/(?P<student_id>\d+)/applied-vacancies', ApplicationViewSet, base_name='applications') - +router.register(r'companies/(?P<company_id>\d+)/applications', CompanyApplicationViewSet, + base_name='company-applications') +router.register(r'companies/(?P<company_id>\d+)/vacancies', CompanyVacanciesViewSet, + base_name='company-vacancies') urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += [ @@ -49,5 +52,3 @@ urlpatterns += [ url(r'^admin/', admin.site.urls), url(r'', apps.index, name="index"), ] - - diff --git a/karma.conf.js b/karma.conf.js index b3bb0e5abb4bb5e70f79c6aa5f4b45db66df36dd..8f79c51c8f9aef0644827c34deae5799134f7c5b 100755 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,7 +10,7 @@ module.exports = function (config) { flags: ['--no-sandbox', '--disable-web-security', '--headless', '--disable-gpu', '--remote-debugging-port=9222'] } }, - singleRun: true, // just run once by default + singleRun: false, // just run once by default frameworks: [ 'mocha', 'chai' ], // use the mocha test framework files: [ 'tests.webpack.js' // just load this file diff --git a/package.json b/package.json index b8171efd8e5106a8fb789aedc11a352187fa35d0..ccd3cef7247924cd935e1083793866dcf9f9b30e 100755 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build-production": "webpack -p --config webpack.prod.config.js --progress --colors", "webpack": "webpack --progress --display-error-details --config webpack.config.js --watch", "watch": "node server.js", - "karma": "karma start" + "karma": "karma start --single-run", + "test": "karma start --auto-watch" }, "author": "", "license": "ISC", @@ -46,6 +47,7 @@ "karma-webpack": "^2.0.3", "mocha": "^3.2.0", "mock-cookie": "^0.3.2", + "moment": "^2.18.1", "react-hot-loader": "^3.0.0-beta.5", "react-test-utils": "0.0.1", "webpack": "^1.13.2", @@ -57,9 +59,10 @@ "babel-core": "^6.24.0", "babel-preset-react": "^6.16.0", "object-assign": "^4.1.1", - "react": "^15.4.2", - "react-addons-test-utils": "^15.4.2", - "react-dom": "^15.4.2", + "react": "15.4.2", + "react-addons-test-utils": "15.4.2", + "react-datepicker": "^0.44.0", + "react-dom": "15.4.2", "react-router": "^3.0.2", "react-tap-event-plugin": "^2.0.1", "semantic-ui-react": "^0.67.2"