Fakultas Ilmu Komputer UI

Commit 9f6d6c19 authored by Nabila Febri Viola's avatar Nabila Febri Viola
Browse files

Merge branch 'PBI-2/register_modal' into 'PBI-2-register_and_login'

Pbi 2/register modal

See merge request !33
parents 5565f23b 2b3cfb5f
Pipeline #38852 passed with stages
in 3 minutes and 51 seconds
# Generated by Django 3.0.3 on 2020-04-06 17:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_merge_20200309_0633'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='blood_type',
field=models.CharField(blank=True, max_length=3),
),
migrations.AlterField(
model_name='profile',
name='married_status',
field=models.CharField(blank=True, max_length=15),
),
]
......@@ -71,8 +71,8 @@ class Profile(models.Model):
birthdate = models.DateField(null=True)
sex = models.CharField(max_length=1, blank=True)
profession = models.CharField(max_length=140, blank=True)
blood_type = models.CharField(max_length=8, blank=True)
married_status = models.CharField(max_length=8, blank=True)
blood_type = models.CharField(max_length=3, blank=True)
married_status = models.CharField(max_length=15, blank=True)
address = models.CharField(max_length=140, blank=True)
city = models.CharField(max_length=60, blank=True)
......
......@@ -95,6 +95,15 @@ class RegistrationFullSerializer(RegistrationSerializer):
def create(self, validated_data):
user = super().create(validated_data)
profile_data = validated_data['profile']
for field in ('body_weight', 'id_card_no', 'birthplace', 'birthdate', 'sex',
'profession', 'blood_type', 'married_status', 'address',
'city', 'district', 'village', 'phone_no', 'work_address',
'work_email', 'work_phone_no'):
value = profile_data.get(field)
setattr(user.profile, field, value)
user.profile.save()
return user
class Meta:
......@@ -102,7 +111,10 @@ class RegistrationFullSerializer(RegistrationSerializer):
fields = ['email', 'password', 'first_name', 'profile']
extra_kwargs = {
'password': {
'write_only': True
'write_only': True,
'style': {
'input_type': 'password'
},
},
'first_name': {
'required': True
......
......@@ -144,8 +144,8 @@ class RegisterFullAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
user = User.objects.get(email=data['email'])
self.assertEqual(user.first_name, response.data['first_name'])
self.assertEqual(user.profile.id_card_no, response.data['profile']['id_card_no'])
self.assertEqual(user.first_name, data['first_name'])
self.assertEqual(user.profile.id_card_no, data['profile']['id_card_no'])
class AccessTokenAPITestCase(APITestCase):
......
......@@ -7480,6 +7480,11 @@
"readable-stream": "^2.3.6"
}
},
"fn-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz",
"integrity": "sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA=="
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
......@@ -13079,6 +13084,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
......@@ -15834,6 +15844,11 @@
}
}
},
"property-expr": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz",
"integrity": "sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g=="
},
"protocols": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz",
......@@ -18580,6 +18595,11 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"dev": true
},
"synchronous-promise": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.10.tgz",
"integrity": "sha512-6PC+JRGmNjiG3kJ56ZMNWDPL8hjyghF5cMXIFOKg+NiwwEZZIvxTWd0pinWKyD227odg9ygF8xVhhz7gb8Uq7A=="
},
"table": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
......@@ -20545,6 +20565,40 @@
"integrity": "sha512-9SNQpwuEh2NucU83i2KMZnONVudZ86YNcFk9tq74YaqrQfgJWO3yB9uzH1tAg8iqh5c9F5j0wuyJ2z72wcum2w==",
"optional": true
},
"yup": {
"version": "0.28.3",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.28.3.tgz",
"integrity": "sha512-amVkCgFWe5bGjrrUiODkbIzrSwtB8JpZrQYSrfj2YsbRdrV+tn9LquWdZDlfOx2HXyfEA8FGnlwidE/bFDxO7Q==",
"requires": {
"@babel/runtime": "^7.8.7",
"fn-name": "~3.0.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.11",
"property-expr": "^2.0.0",
"synchronous-promise": "^2.0.10",
"toposort": "^2.0.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.8.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz",
"integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz",
"integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g=="
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
}
}
},
"yurnalist": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/yurnalist/-/yurnalist-1.1.2.tgz",
......
......@@ -33,9 +33,10 @@
"react-dom": "^16.12.0",
"react-google-login": "^5.1.1",
"react-helmet": "^5.2.1",
"react-phone-input-2": "^2.12.0",
"react-hook-form": "^5.0.1",
"redux": "^4.0.5"
"react-phone-input-2": "^2.12.0",
"redux": "^4.0.5",
"yup": "^0.28.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.1.1",
......
......@@ -27,3 +27,18 @@ export const postUserLogout = () =>
detail: "SUCCESS",
},
})
export const postRegisterUser = async () =>
await fetch(`${BASE_API_URL}/auth/register-full/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: data,
})
.then(async response => {
return await response.json()
})
.then(async data => {
return data
})
This diff is collapsed.
import React from "react"
import { cleanup, fireEvent, render } from "@testing-library/react"
import CompleteProfile from "./complete-profile"
it("captures clicks", async () => {
const { findByText } = render(<CompleteProfile show />)
expect(await findByText("Lengkapi Profil")).toBeInTheDocument()
const next = await findByText("Simpan")
fireEvent.click(next)
})
afterEach(cleanup)
import React from "react"
import { Form } from "react-bootstrap"
import { useForm } from "react-hook-form"
import * as yup from "yup"
import { BASE_API_URL } from "../config"
import ModalRegisterAccount from "./register-account"
const ModalRegister = ({ show, handleClose }) => {
const schema = yup.object().shape({
email: yup
.string()
.email("Masukkan email yang valid.")
.required("Masukkan email."),
password: yup
.string()
.min(8, "Password terlalu pendek - setidaknya harus 8 karakter.")
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
"Password harus terdiri dari minimal 1 huruf kecil, 1 huruf kapital, 1 angka, dan 1 karakter spesial (!@#$%^&*)."
)
.required("Masukkan password."),
passwordConfirmation: yup
.string()
.oneOf([yup.ref("password"), null], "Password harus sama.")
.required("Masukkan konfirmasi password."),
first_name: yup
.string()
.matches(
/^(?=^.{5,70}$)^[A-Za-z]+([\ A-Za-z]+)*$/,
"Masukkan nama yang valid."
)
.required("Masukkan nama."),
body_weight: yup
.number()
.positive("Masukkan berat badan yang valid.")
.moreThan(1, "Masukkan berat badan yang valid.")
.lessThan(200, "Masukkan berat badan yang valid.")
.required("Masukkan berat badan."),
id_card_no: yup
.string()
.matches(/^\d{16}$/, "Masukkan nomor kartu pengenal yang valid.")
.required("Masukkan nomor kartu pengenal."),
birthplace: yup.string().required("Masukkan tempat kelahiran."),
// birthdate: yup.date().required("Masukkan tanggal lahir"),
sex: yup.string().required("Pilih jenis kelamin."),
address: yup.string().required("Masukkan alamat."),
city: yup.string().required("Masukkan nama kota."),
district: yup.string().required("Masukkan nama kecamatan."),
village: yup.string().required("Masukkan nama kelurahan."),
phone_no: yup.number().required("Masukkan nomor telepon."),
work_address: yup.string().required("Masukkan alamat kantor."),
work_email: yup
.string()
.email()
.required("Masukkan email kantor."),
work_phone_no: yup.number().required("Masukkan nomor telepon kantor."),
})
const {
control,
register,
handleSubmit,
errors,
triggerValidation,
} = useForm({
mode: "onChange",
validationSchema: schema,
})
const onSubmit = async data => {
data =
'{"email": "' +
data.email +
'", "password": "' +
data.password +
'", "first_name": "' +
data.first_name +
'", "profile": ' +
JSON.stringify(data) +
"}"
await fetch(`${BASE_API_URL}/auth/register-full/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: data,
})
.then(async response => {
return await response.json()
})
.then(async data => {
await window.alert(
"Silakan verifikasi email terlebih dahulu di" +
data.email +
" untuk login."
),
await location.reload()
})
}
return (
<>
<ModalRegisterAccount
show={show}
handleClose={handleClose}
register={register}
errors={errors}
control={control}
triggerValidation={triggerValidation}
handleSubmit={handleSubmit}
onSubmit={onSubmit}
/>
</>
)
}
export default ModalRegister
......@@ -2,6 +2,7 @@ import React, { useState } from "react"
import { Navbar as BNavbar, Nav, Button } from "react-bootstrap"
import { Link as GatsbyLink } from "gatsby"
import ModalLogin from "./modal-login"
import ModalRegister from "./modal-register"
import "./navbar.scss"
import { useAuth } from "../hooks/authenticate"
......@@ -13,6 +14,7 @@ const NavLink = ({ className = "", ...props }) => (
const Navbar = () => {
const { user, logout } = useAuth()
const [showModalLogin, setShowModalLogin] = useState(false)
const [showModalRegister, setShowModalRegister] = useState(false)
return (
<>
......@@ -29,7 +31,7 @@ const Navbar = () => {
<NavLink to="#">Home</NavLink>
<NavLink to="/jadwal-donor">Jadwal Donor</NavLink>
<NavLink to="#">Ajukan Acara Donor</NavLink>
<NavLink to="#">Profil</NavLink>
{user ? <NavLink to="#">Profil</NavLink> : <></>}
</Nav>
{user ? (
<Nav>
......@@ -41,7 +43,12 @@ const Navbar = () => {
) : (
<Nav>
<NavLink onClick={() => setShowModalLogin(true)}>Masuk</NavLink>
<Button className="btn btn-cream">Daftar</Button>
<Button
className="btn btn-cream"
onClick={() => setShowModalRegister(true)}
>
Daftar
</Button>
</Nav>
)}
</BNavbar.Collapse>
......@@ -50,6 +57,10 @@ const Navbar = () => {
show={showModalLogin}
handleClose={() => setShowModalLogin(false)}
/>
<ModalRegister
show={showModalRegister}
handleClose={() => setShowModalRegister(false)}
/>
</>
)
}
......
......@@ -134,3 +134,38 @@ describe(`Login`, () => {
expect(await screen.findByText(/Masuk/i)).toBeInTheDocument()
})
})
describe(`Register`, () => {
it(`shows "Register" modal when user clicked "Daftar" button`, async () => {
const { getByText } = render(
<AuthProvider>
<Navbar />
</AuthProvider>
)
fireEvent.click(getByText("Daftar"))
expect(getByText("Email")).toBeInTheDocument()
expect(getByText("Password")).toBeInTheDocument()
expect(getByText("Konfirmasi Password")).toBeInTheDocument()
fireEvent.click(getByText("×"))
await waitForElementToBeRemoved(() => getByText("Email"))
})
it(`shows error messages when user submitted empty fields`, async () => {
const { getByText, findByText, getByTestId } = render(
<AuthProvider>
<Navbar />
</AuthProvider>
)
fireEvent.click(getByText("Daftar"))
fireEvent.click(getByTestId("btn-register"))
expect(await findByText(/Masukkan email./)).toBeInTheDocument()
expect(
await findByText(/Password terlalu pendek - setidaknya harus 8 karakter./)
).toBeInTheDocument()
expect(
await findByText(/Masukkan konfirmasi password./)
).toBeInTheDocument()
})
})
import React, { useState } from "react"
import { Modal, Button, Form } from "react-bootstrap"
import ModalCompleteProfile from "./complete-profile"
const ModalRegisterAccount = ({
show,
handleClose,
register,
errors,
control,
triggerValidation,
handleSubmit,
onSubmit,
}) => {
const [showModalCompleteProfile, setShowModalCompleteProfile] = useState(
false
)
return (
<>
<Modal
aria-labelledby="contained-modal-title-vcenter"
centered
show={show}
onHide={handleClose}
>
<Modal.Header
bsPrefix="border-0 d-flex flex-row justify-content-end mt-2 mr-3"
closeButton
onClick={handleClose}
></Modal.Header>
<Modal.Body>
<h2 className="text-center text-red mb-5 mt-2">
<b>
<b>Daftar Akun</b>
</b>
</h2>
<Form.Group controlId="formRegEmail">
<div className="row d-flex flex-row justify-content-center">
<div className="col-md-3">
<Form.Label className="text-red">
<b>Email</b>
</Form.Label>
</div>
<div className="col-md-8 justify-content-center align-items-center d-flex flex-column">
<Form.Control
name="email"
size="lg"
type="email"
ref={register}
/>
{errors.email && (
<Form.Control.Feedback type="invalid">
{errors.email.message}
</Form.Control.Feedback>
)}
</div>
</div>
</Form.Group>
<Form.Group controlId="formRegPassword">
<div className="row d-flex flex-row justify-content-center">
<div className="col-md-3">
<Form.Label className="text-red">
<b>Password</b>
</Form.Label>
</div>
<div className="col-md-8 justify-content-center align-items-center d-flex flex-column">
<Form.Control
name="password"
size="lg"
type="password"
ref={register}
/>
{errors.password && (
<Form.Control.Feedback type="invalid">
{errors.password.message}
</Form.Control.Feedback>
)}
</div>
</div>
</Form.Group>
<Form.Group controlId="formRegConfirmPassword">
<div className="row d-flex flex-row justify-content-center">
<div className="col-md-3">
<Form.Label className="text-red">
<b>Konfirmasi Password</b>
</Form.Label>
</div>
<div className="col-md-8 justify-content-center align-items-center d-flex flex-column">
<Form.Control
name="passwordConfirmation"
size="lg"
type="password"
ref={register}
/>
{errors.passwordConfirmation && (
<Form.Control.Feedback type="invalid">
{errors.passwordConfirmation.message}
</Form.Control.Feedback>
)}
</div>
</div>
</Form.Group>
<div className="row px-5 mt-5 mb-3">
<div className="text-right col-md-12 mr-3 ml-4">
<Button
data-testid="btn-register"
variant="btn btn-red shadow"
onClick={async () => {
await triggerValidation()
if (!errors.email && !errors.passwordConfirmation) {
setShowModalCompleteProfile(true)
}
}}
>
Daftar
</Button>
</div>
</div>
</Modal.Body>
</Modal>
<ModalCompleteProfile
show={showModalCompleteProfile}
handleClose={() => setShowModalCompleteProfile(false)}
register={register}
errors={errors}
control={control}
handleSubmit={handleSubmit}
onSubmit={onSubmit}
/>
</>
)
}
export default ModalRegisterAccount
.modal-header {
border-bottom: 0 none;
@import "src/styles/global.scss";
h5 {
margin-top: 0.5rem;
}
.form-control {
font-size: 14px;
margin-top: 0.5rem;
}
.modal-footer {
border-top: 0 none;
.react-tel-input .form-control {
width: 100%;
height: calc(1.5em + 1rem + 2px);
font-size: 15px;
line-height: 1.5;
border-radius: 0.3rem;
}
.border-form {
border: solid #a91320;
.react-tel-input .flag-dropdown {
outline: $red;
background-color: $dark-red;
border: 2px $red;
border-radius: 5px 0 0 5px;
}
h5 {
color: #a91320;
margin-top: 1rem;
.react-tel-input .selected-flag .arrow {