Fakultas Ilmu Komputer UI

Commit 233b4789 authored by Nabila Febri Viola's avatar Nabila Febri Viola
Browse files

[CHORES] Fix conflict

parents c198af1c 98045e93
......@@ -10,7 +10,7 @@ variables:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules/
- .npm
- .cache/pip/
DRF Unit Tests:
......@@ -35,8 +35,7 @@ Gatsby Unit Tests:
coverage: '/All files\s*\|\s*[\d\.]+\s*\|\s*[\d\.]+\s*\|\s*[\d\.]+\s*\|\s*([\d\.]+)/'
before_script:
- cd frontend
- npm install
- export PATH=$PATH:$(npm bin)
- npm ci --cache .npm --prefer-offline
script:
- npm run test
artifacts:
......@@ -59,8 +58,8 @@ Gatsby ESLint Lint:
image: node:12
before_script:
- cd frontend
- npm i -g eslint
- npm i eslint-plugin-prettier eslint-config-react-app @typescript-eslint/eslint-plugin@2.x @typescript-eslint/parser@2.x babel-eslint@10.x eslint@6.x eslint-plugin-flowtype@3.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@1.x
- npm install --cache .npm --prefer-offline -g eslint
- npm ci --cache .npm --prefer-offline
script:
- eslint .
only:
......@@ -119,44 +118,38 @@ DRF Static Deploy:
stage: deploy
image: node:12
tags:
- deploy
- public
needs:
- job: DRF Static Build
artifacts: true
before_script:
- npm install -g netlify-cli
- npm install -g --cache .npm --prefer-offline netlify-cli
script:
- netlify deploy --site=$NETLIFY_DRF_ID --dir=static_root/ --auth=$NETLIFY_DRF_AUTH --prod
only:
refs:
- master
- staging
changes:
- backend/**/*
- .gitlab-ci.yml
DRF Deploy Staging:
stage: deploy
tags:
- deploy
- no_proxy
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
needs: ["DRF Unit Tests"]
script:
- echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$CONTAINER_REGISTRY_AUTH\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR/backend/ --dockerfile $CI_PROJECT_DIR/backend/Dockerfile --destination "$CONTAINER_REGISTRY_PROJECT:$CI_COMMIT_REF_NAME"
- /kaniko/executor --context $CI_PROJECT_DIR/backend/ --dockerfile $CI_PROJECT_DIR/backend/dev.Dockerfile --destination "$CONTAINER_REGISTRY_PROJECT:$CI_COMMIT_REF_NAME"
only:
refs:
- staging
changes:
- backend/**/*
- .gitlab-ci.yml
DRF Deploy Production:
stage: deploy
tags:
- deploy
- no_proxy
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
......@@ -167,9 +160,6 @@ DRF Deploy Production:
only:
refs:
- master
changes:
- backend/**/*
- .gitlab-ci.yml
Gatsby Deploy Staging:
stage: deploy
......@@ -179,9 +169,9 @@ Gatsby Deploy Staging:
needs: ["Gatsby Unit Tests"]
before_script:
- cd frontend
- npm install
- npm install netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=staging gatsby build
- npm ci --cache .npm --prefer-offline
- npm install --cache .npm --prefer-offline netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=staging GATSBY_GOOGLE_CLIENT_ID=$GATSBY_GOOGLE_CLIENT_ID_STAGING gatsby build
script:
- netlify deploy --site=$NETLIFY_GATSBY_ID_STAGING --dir=public/ --auth=$NETLIFY_GATSBY_AUTH --prod
only:
......@@ -196,9 +186,9 @@ Gatsby Deploy Production:
needs: ["Gatsby Unit Tests"]
before_script:
- cd frontend
- npm install
- npm install netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=production gatsby build
- npm ci --cache .npm --prefer-offline
- npm install --cache .npm --prefer-offline netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=production GATSBY_GOOGLE_CLIENT_ID=$GATSBY_GOOGLE_CLIENT_ID_PRODUCTION gatsby build
script:
- netlify deploy --site=$NETLIFY_GATSBY_ID --dir=public/ --auth=$NETLIFY_GATSBY_AUTH --prod
only:
......
README.md
Dockerfile
*Dockerfile
.dockerignore
**/__pycache__/*
.venv/
.env/
venv/
env/
.vscode/
.netlify/
static_root/
db.sqlite3
.coverage
coverage.xml
.flake8
.gitignore
sonar-project.properties
......@@ -3,9 +3,10 @@
.env/
venv/
env/
.vscode/
static_root/
.netlify/
db.sqlite3
.coverage
coverage.xml
.idea/
FROM python:3.8-buster
FROM python:3.8-alpine as builder
RUN apt-get update -q && \
apt-get install -y libpq-dev python3-dev
RUN apk add --no-cache postgresql-dev gcc python3-dev musl-dev
WORKDIR /app/
COPY requirements.txt ./
COPY requirements-prod.txt ./
RUN pip wheel --no-cache-dir \
--no-deps \
--wheel-dir /app/wheels \
-r requirements-prod.txt
FROM python:3.8-alpine
RUN apk add --no-cache libpq postgresql-client
WORKDIR /app
COPY requirements.txt /app/requirements.txt
COPY requirements-prod.txt /app/requirements-prod.txt
RUN pip install -r requirements-prod.txt
COPY --from=builder /app/wheels/ /wheels
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*
COPY dblood/ /app/dblood/
COPY main/ /app/main/
COPY manage.py /app/
COPY . /app/
EXPOSE 8000
ENV DATABASE_URL 'sqlite:///db.sqlite3'
ENV DJANGO_SETTINGS_MODULE 'dblood.settings.production'
ENV DEBUG 'False'
CMD ["gunicorn", "dblood.wsgi", "--bind", "0.0.0.0:8000"]
......@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dblood.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dblood.settings.production')
application = get_asgi_application()
......@@ -21,19 +21,6 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'SECRET_KEY', '__super_secret_high_entropy_pseudo_random_bytes_a608e1fb__')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True') != 'False'
if DEBUG:
ALLOWED_HOSTS = ['*']
else:
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(';')
# Application definition
INSTALLED_APPS = [
......@@ -44,13 +31,13 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'anymail',
'corsheaders',
'rest_framework_authlib',
'main',
'stok_darah',
'anymail',
'django_extensions',
'corsheaders',
'donor'
'donor',
]
MIDDLEWARE = [
......@@ -137,7 +124,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Jakarta'
USE_I18N = True
......@@ -149,11 +136,6 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
if DEBUG:
STATIC_URL = '/static/'
else:
STATIC_URL = os.getenv('STATIC_URL', 'https://dblood-api.netlify.com/')
STATIC_ROOT = 'static_root/'
CORS_ORIGIN_WHITELIST = [
......@@ -163,14 +145,6 @@ CORS_ORIGIN_WHITELIST = [
"https://dblood.netlify.com",
]
if DEBUG:
REST_CLIENT_SITE = 'http://localhost:8000'
else:
REST_CLIENT_SITE = 'https://dblood.netlify.com'
REST_CLIENT_SITE = os.getenv('REST_CLIENT_SITE', REST_CLIENT_SITE)
# REST Framework settings
REST_FRAMEWORK = {
......@@ -179,6 +153,15 @@ REST_FRAMEWORK = {
)
}
# Authlib settings
AUTHLIB_OAUTH_CLIENTS = {
'google': {
'client_id': os.getenv('GOOGLE_CLIENT_ID'),
'client_secret': os.getenv('GOOGLE_CLIENT_SECRET'),
}
}
# Email settings
ANYMAIL = {
......
from .common import *
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True') != 'False'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(';')
# Application definition
STATIC_URL = os.getenv('STATIC_URL', 'https://dblood-api.netlify.app/')
REST_CLIENT_SITE = 'https://dblood.netlify.app'
REST_CLIENT_SITE = os.getenv('REST_CLIENT_SITE', REST_CLIENT_SITE)
from .production import *
SECRET_KEY = os.getenv('SECRET_KEY', '__super_secret_high_entropy_pseudo_random_bytes_a608e1fb__')
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS.extend([
'django_extensions',
])
STATIC_URL = '/static/'
REST_CLIENT_SITE = 'http://localhost:8000'
REST_CLIENT_SITE = os.getenv('REST_CLIENT_SITE', REST_CLIENT_SITE)
......@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dblood.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dblood.settings.production')
application = get_wsgi_application()
FROM python:3.8-buster
RUN apt-get update -q && \
apt-get install -y libpq-dev python3-dev
WORKDIR /app
COPY requirements.txt /app/
COPY requirements-dev.txt /app/
RUN pip install -r requirements-dev.txt
COPY . /app/
EXPOSE 8000
ENV DATABASE_URL 'sqlite:///db.sqlite3'
RUN ["python3", "manage.py", "collectstatic"]
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
......@@ -5,4 +5,4 @@ from donor.models import JadwalDonor
@admin.register(JadwalDonor)
class JadwalDonorAdmin(admin.ModelAdmin):
pass
list_filter = ('kecamatan', 'time_start', 'category')
......@@ -18,9 +18,9 @@ class Command(BaseCommand):
start_date = date.today() - timedelta(days=10)
for cur_date in (start_date + timedelta(days=i) for i in range(101)):
for _ in range(random.randint(1, 3)):
JadwalDonor.objects.create_jadwal_donor(
JadwalDonor.objects.create(
kecamatan=random.choice(JadwalDonor.Kecamatan.choices)[0],
location=fake.address(),
location=fake.address()[:30], # Cut to 30 chars
time_start=make_aware(datetime.combine(
cur_date, time(hour=random.randint(8, 12)))),
time_end=make_aware(datetime.combine(
......
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.timezone import localtime
class JadwalDonor(models.Model):
......@@ -37,4 +38,6 @@ class JadwalDonor(models.Model):
return super().save(*args, **kwargs)
def __str__(self):
return f"{self.location}, {self.time_start.date()} ({self.time_start.time()} - {self.time_end.time()})"
time_start = localtime(self.time_start)
time_end = localtime(self.time_end)
return f"{self.location}, {time_start.date()} ({time_start.time()} - {time_end.time()})"
......@@ -7,8 +7,8 @@ from django.core.exceptions import ValidationError
class JadwalDonorTest(TestCase):
def test_create_jadwal_donor_success(self):
time_start = '2020-03-02T07:08:00+00:00'
time_end = '2020-03-02T10:08:00+00:00'
time_start = '2020-03-02T07:08:00+07:00'
time_end = '2020-03-02T10:08:00+07:00'
jadwal_donor = JadwalDonor.objects.create(
kecamatan="Beji", location='Fasilkom', time_start=datetime.fromisoformat(time_start),
time_end=datetime.fromisoformat(time_end), quota=150, category=JadwalDonor.Category.PUBLIC
......@@ -16,8 +16,8 @@ class JadwalDonorTest(TestCase):
self.assertEqual(str(jadwal_donor), f"Fasilkom, 2020-03-02 (07:08:00 - 10:08:00)")
def test_create_jadwal_donor_start_greater_than_end_fail(self):
time_start = '2020-03-02T10:08:00+00:00'
time_end = '2020-03-02T07:08:00+00:00'
time_start = '2020-03-02T10:08:00+07:00'
time_end = '2020-03-02T07:08:00+07:00'
with self.assertRaisesMessage(ValidationError, "time_start should be less than time_end"):
JadwalDonor.objects.create(
kecamatan="Beji", location='Pacil', time_start=datetime.fromisoformat(time_start),
......
......@@ -7,9 +7,9 @@ from donor.models import JadwalDonor
class JadwalDonorTests(APITestCase):
def test_get_jadwal_donor_sorted_by_time_start(self):
time_start_1 = '2020-03-02T06:00:00Z'
time_start_2 = '2020-03-02T07:00:00Z'
time_end = '2020-03-02T10:00:00Z'
time_start_1 = '2020-03-02T06:00:00+07:00'
time_start_2 = '2020-03-02T07:00:00+07:00'
time_end = '2020-03-02T10:00:00+07:00'
# insert time_start_2 first to make sure it is not sorted by id
JadwalDonor.objects.create(
kecamatan="Beji", location="Depok Baru", time_start=time_start_2,
......@@ -44,10 +44,10 @@ class JadwalDonorTests(APITestCase):
)
def test_get_jadwal_donor_filtered_by_date(self):
time_start_1 = '2020-03-02T06:00:00Z'
time_start_2 = '2020-03-03T07:00:00Z'
time_end_1 = '2020-03-02T10:00:00Z'
time_end_2 = '2020-03-03T10:00:00Z'
time_start_1 = '2020-03-02T06:00:00+07:00'
time_start_2 = '2020-03-03T07:00:00+07:00'
time_end_1 = '2020-03-02T10:00:00+07:00'
time_end_2 = '2020-03-03T10:00:00+07:00'
JadwalDonor.objects.create(
kecamatan="Beji", location="Depok", time_start=time_start_1,
time_end=time_end_1, quota=150, category=JadwalDonor.Category.PUBLIC
......
......@@ -120,3 +120,34 @@ class RegistrationFullSerializer(RegistrationSerializer):
'required': True
},
}
class UserProfileSerializer(ModelSerializer):
profile = ProfilePartialSerializer()
def update(self, instance, validated_data):
profile_data = validated_data.pop('profile')
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.save()
# Handle nested instance
profile = instance.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, getattr(profile, field))
setattr(profile, field, value)
profile.save()
return instance
class Meta:
model = User
fields = ('email', 'first_name', 'profile')
extra_kwargs = {
'email': {
'read_only': True
},
}
from unittest.mock import patch
from django.conf import settings
from django.core import mail
from rest_framework import status
from rest_framework.test import APITestCase
from rest_framework.exceptions import ErrorDetail
from rest_framework_authlib.tokens import AccessToken
from urllib.parse import urljoin
from .factories import UserFactory
......@@ -96,6 +98,7 @@ class EmailVerificationView(APITestCase):
self.assertRedirects(response,
urljoin(settings.REST_CLIENT_SITE, '/email-verification/failed'),
fetch_redirect_response=False)
def test_register_without_name(self):
data = {
'email': 'donald@duckduckgo.org',
......@@ -160,3 +163,93 @@ class AccessTokenAPITestCase(APITestCase):
self.assertIn('access', response.data)
self.assertNotIn('password', response.data)
class UserProfileViewTestCase(APITestCase):
def setUp(self):
self.user = UserFactory()
self.user.save()
token = str(AccessToken.for_user(self.user))
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
def test_get_user_profile(self):
response = self.client.get('/user/profile/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['email'], self.user.email)
self.assertEqual(response.data['first_name'], self.user.first_name)
def test_put_new_user_profile(self):
response = self.client.get('/user/profile/')
data = response.data
data['first_name'] = 'Donald Duck'
data['profile']['city'] = 'Kendari'
response = self.client.put('/user/profile/', data=data, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['first_name'], data['first_name'])
self.assertEqual(response.data['profile']['city'],
data['profile']['city'])
def test_put_new_user_profile_with_email(self):
response = self.client.get('/user/profile/')
data = response.data
data['email'] = 'donald@10minutesmail.xyz'
response = self.client.put('/user/profile/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotEqual(response.data['email'], data['email'])
class OAuthAccessTokenViewTestCase(APITestCase):
def setUp(self):
from authlib.jose import jwt
self.keys = ('somebody_else_key', 'totally_secret_key')
self.token = jwt.encode(
{
'alg': 'HS256',
},
{
'email': 'totallyfakedonald@gmail.com',
'name': 'Donald The Duck',
},
self.keys[1]).decode('utf-8')
self.bad_token = jwt.encode(
{
'alg': 'HS256',
},
{
'Email': 'totallyfakedonald@gmail.com',
'Name': 'Donald The Duck',
},
self.keys[1]).decode('utf-8')
def test_get_access_token(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'tokenId': self.token})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get_access_token_bad(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'tokenId': self.bad_token})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_get_access_token_no_token(self):
with patch('main.views.OAuthAccessTokenView.get_keys') as fake_keys:
fake_keys.return_value = self.keys
response = self.client.post('/auth/access/oauth/',
data={'token_id': self.token})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
......@@ -4,17 +4,26 @@ from rest_framework_authlib.views import AccessTokenView
from .views import (
EmailVerificationView,
HelloView,
OAuthAccessTokenView,
RegisterFullView,
RegisterView,
SecretView,
UserProfileView,
)
urlpatterns = [
path('hello/', HelloView.as_view()),
path('secret/', SecretView.as_view()),
# Auth related path
path('auth/register/', RegisterView.as_view()),
path('auth/register-full/', RegisterFullView.as_view()),
path('auth/access/', AccessTokenView.as_view()),
path('auth/access/oauth/', OAuthAccessTokenView.as_view()),
path('auth/email-verification/', EmailVerificationView.as_view(),
name='email-verification'),
# Misc PoC path
path('hello/', HelloView.as_view()),
path('secret/', SecretView.as_view()),