Fakultas Ilmu Komputer UI

Commit 98045e93 authored by Giovan Isa Musthofa's avatar Giovan Isa Musthofa
Browse files

Merge branch 'PBI-2-register_and_login' into 'staging'

Pbi 2 register and login

Closes #2

See merge request !36
parents 2a7cc8e0 f4bc83ee
Pipeline #38919 passed with stages
in 24 minutes and 1 second
......@@ -22,13 +22,12 @@ DRF Unit Tests:
- export DEBUG=True
- pip install -r requirements-dev.txt
script:
- coverage run --include='main/*','donor/*' manage.py test
- coverage report
- coverage run --include='main/*,stok_darah/*,donor/*,rest_framework_authlib/*' manage.py test
- coverage report --show-missing
- coverage xml
artifacts:
paths:
- backend/coverage.xml
only:
Gatsby Unit Tests:
stage: tests
......@@ -172,7 +171,7 @@ Gatsby Deploy Staging:
- cd frontend
- npm ci --cache .npm --prefer-offline
- npm install --cache .npm --prefer-offline netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=staging gatsby build
- 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:
......@@ -189,7 +188,7 @@ Gatsby Deploy Production:
- cd frontend
- npm ci --cache .npm --prefer-offline
- npm install --cache .npm --prefer-offline netlify-cli gatsby-cli -g
- GATSBY_ACTIVE_ENV=production gatsby build
- 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:
......
......@@ -9,3 +9,4 @@ static_root/
db.sqlite3
.coverage
coverage.xml
.idea/
{
"python.pythonPath": "venv/bin/python3"
}
\ No newline at end of file
......@@ -47,8 +47,8 @@ PS C:\Users\Giovan\Projects\mantan-aab-d-blood\drf> Set-ExecutionPolicy -Scope C
5. Run unit tests and measure coverage
```
(venv) PS C:\Users\Giovan\Projects\mantan-aab-d-blood\drf> coverage run --include='main/*','donor/*' manage.py test
(venv) PS C:\Users\Giovan\Projects\mantan-aab-d-blood\drf> coverage report
(venv) PS C:\Users\Giovan\Projects\mantan-aab-d-blood\drf> coverage run --include='main/*,stok_darah/*,donor/*, rest_framework_authlib/*' manage.py test
(venv) PS C:\Users\Giovan\Projects\mantan-aab-d-blood\drf> coverage report --show-missing
```
6. Run flake8 linter
......
......@@ -31,8 +31,13 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'anymail',
'corsheaders',
'donor'
'rest_framework_authlib',
'main',
'stok_darah',
'donor',
]
MIDDLEWARE = [
......@@ -75,6 +80,26 @@ DATABASES = {
}
# Authentication settings
AUTH_USER_MODEL = 'main.User'
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
# 'rest_framework_authlib.authentication.JWTAuthenticationBackend',
]
# Authlib settings
AUTHLIB_OAUTH_CLIENTS = {
'google': {
'client_id': os.getenv('GOOGLE_CLIENT_ID'),
'client_secret': os.getenv('GOOGLE_CLIENT_SECRET'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
......@@ -111,12 +136,40 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = os.getenv('STATIC_URL', 'https://dblood-api.netlify.com/')
STATIC_ROOT = 'static_root/'
CORS_ORIGIN_WHITELIST = [
"http://localhost:8000",
"https://dblood-staging.netlify.com",
"https://dblood.netlify.com",
"https://dblood-staging.netlify.app",
"https://dblood.netlify.app",
]
# REST Framework settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_authlib.authentication.JWTAuthentication',
)
}
# Authlib settings
AUTHLIB_OAUTH_CLIENTS = {
'google': {
'client_id': os.getenv('GOOGLE_CLIENT_ID'),
'client_secret': os.getenv('GOOGLE_CLIENT_SECRET'),
}
}
# Email settings
ANYMAIL = {
'MAILGUN_API_KEY': os.getenv('MAILGUN_API_KEY'),
'MAILGUN_SENDER_DOMAIN': os.getenv('MAILGUN_SENDER_DOMAIN'),
}
DEFAULT_FROM_EMAIL = 'noreply@dblood.depok.go.id'
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
SERVER_EMAIL = 'server@dblood.depok.go.id'
......@@ -11,4 +11,8 @@ ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(';')
# Application definition
STATIC_URL = os.getenv('STATIC_URL', 'https://dblood-api.netlify.com/')
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)
......@@ -14,3 +14,7 @@ INSTALLED_APPS.extend([
])
STATIC_URL = '/static/'
REST_CLIENT_SITE = 'http://localhost:8000'
REST_CLIENT_SITE = os.getenv('REST_CLIENT_SITE', REST_CLIENT_SITE)
......@@ -20,7 +20,7 @@ class Command(BaseCommand):
for _ in range(random.randint(1, 3)):
JadwalDonor.objects.create(
kecamatan=random.choice(JadwalDonor.Kecamatan.choices)[0],
location=fake.address()[:30], # Cut to 30 chars
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(
......
default_app_config = 'main.apps.MainConfig'
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.utils.translation import ugettext_lazy as _
from .models import User
@admin.register(User)
class UserAdmin(DjangoUserAdmin):
"""Define admin model for custom User model with no username field."""
fieldsets = (
(None, {'fields': ('email', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
list_display = ('email', 'first_name', 'last_name', 'is_staff')
search_fields = ('email', 'first_name', 'last_name')
ordering = ('email',)
......@@ -3,3 +3,7 @@ from django.apps import AppConfig
class MainConfig(AppConfig):
name = 'main'
verbose_name = "D'Blood Main"
def ready(self):
from . import signals # NOQA
import factory
import factory.django
import factory.fuzzy
import itertools
from . import models
LOCALE = 'id_ID'
BLOOD_LETTERS = ('A', 'B', 'O', 'AB')
RHESUS = '+-'
BLOOD_TYPES = list(l + r for l, r in itertools.product(BLOOD_LETTERS, RHESUS))
class ProfileFactory(factory.Factory):
class Meta:
model = models.Profile
# We pass in profile=None to prevent UserFactory from creating another
# profile (this disables the RelatedFactory)
user = factory.SubFactory('main.factories.UserFactory', profile=None)
body_weight = factory.fuzzy.FuzzyFloat(40, 110)
id_card_no = factory.Faker('lexify', text='#################')
birthplace = factory.Faker('city', locale=LOCALE)
birthdate = factory.Faker('date_of_birth')
sex = factory.fuzzy.FuzzyChoice(models.Sex.choices, getter=lambda c: c[0])
profession = factory.Faker('job')
blood_type = factory.Faker('random_element', elements=BLOOD_TYPES)
married_status = factory.fuzzy.FuzzyChoice(
models.MarriedStatus.choices,
getter=lambda c: c[0])
address = factory.Faker('address', locale=LOCALE)
city = factory.Faker('city', locale=LOCALE)
district = factory.Faker('city', locale=LOCALE)
village = factory.Faker('city', locale=LOCALE)
phone_no = factory.Faker('phone_number', locale=LOCALE)
work_address = factory.Faker('address', locale=LOCALE)
work_email = factory.Faker('company_email', locale=LOCALE)
work_phone_no = factory.Faker('phone_number', locale=LOCALE)
class UserFactory(factory.Factory):
class Meta:
model = models.User
# We pass in 'user' to link the generated Profile to our just-generated
# User. This will call ProfileFactory(user=our_new_user), thus skipping the
# SubFactory.
profile = factory.RelatedFactory(ProfileFactory, 'user')
email = factory.Faker('email', locale=LOCALE)
first_name = factory.Faker('first_name', locale=LOCALE)
last_name = factory.Faker('last_name', locale=LOCALE)
# Generated by Django 3.0.3 on 2020-02-29 09:45
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import main.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', main.models.UserManager()),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('id_card_no', models.CharField(blank=True, max_length=20)),
('birthplace', models.CharField(blank=True, max_length=60)),
('birthdate', models.DateField(null=True)),
('sex', models.CharField(blank=True, max_length=1)),
('profession', models.CharField(blank=True, max_length=140)),
('blood_type', models.CharField(blank=True, max_length=8)),
('married_status', models.CharField(blank=True, max_length=8)),
('address', models.CharField(blank=True, max_length=140)),
('city', models.CharField(blank=True, max_length=60)),
('district', models.CharField(blank=True, max_length=60)),
('village', models.CharField(blank=True, max_length=60)),
('phone_no', models.CharField(blank=True, max_length=20)),
('work_address', models.CharField(blank=True, max_length=140)),
('work_email', models.EmailField(blank=True, max_length=254)),
('work_phone_no', models.CharField(blank=True, max_length=20)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 3.0.3 on 2020-03-09 05:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='profile',
name='body_weight',
field=models.FloatField(null=True),
),
]
# Generated by Django 3.0.3 on 2020-03-07 07:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='is_verified',
field=models.BooleanField(default=False),
),
]
# Generated by Django 3.0.3 on 2020-03-09 06:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0002_user_is_verified'),
('main', '0002_profile_body_weight'),
]
operations = [
]
# 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),
),
]
from django.conf import settings
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
# Create your models here.
class UserManager(BaseUserManager):
"""Define a model manager for User model with no username field."""
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""Create and save a User with the given email and password."""
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
"""Create and save a regular User with the given email and password."""
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
"""Create and save a SuperUser with the given email and password."""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True')
return self._create_user(email, password, **extra_fields)
class User(AbstractUser):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserManager()
username = None
email = models.EmailField(_('email address'), unique=True)
is_verified = models.BooleanField(default=False)
class Sex(models.TextChoices):
MALE = 'M', _('Male')
FEMALE = 'F', _('Female')
class MarriedStatus(models.TextChoices):
SINGLE = 'S', _('Single')
MARRIED = 'M', _('Married')
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
body_weight = models.FloatField(null=True)
id_card_no = models.CharField(max_length=20, blank=True)
birthplace = models.CharField(max_length=60, blank=True)
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=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)
district = models.CharField(max_length=60, blank=True)
village = models.CharField(max_length=60, blank=True)
phone_no = models.CharField(max_length=20, blank=True)
work_address = models.CharField(max_length=140, blank=True)
work_email = models.EmailField(blank=True)
work_phone_no = models.CharField(max_length=20, blank=True)
def __str__(self):
return f'({self.sex}, {self.blood_type})'
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import (
CharField,
Serializer,
ModelSerializer,
ValidationError,
)
from rest_framework_authlib.errors import TokenError
from .tokens import EmailVerificationToken
from .models import (
Profile,
User,
)
class RegistrationSerializer(ModelSerializer):
def create(self, validated_data):
user = User.objects.create_user(
validated_data[User.USERNAME_FIELD], validated_data['password'],
first_name=validated_data['first_name'])
return user
class Meta:
model = User
fields = ['email', 'password', 'first_name']
extra_kwargs = {
'password': {
'write_only': True,
'style': {
'input_type': 'password'
}
},
'first_name': {
'required': True
},
}
class EmailVerificationTokenSerializer(Serializer):
email_verification = CharField(style={'input_type': 'hidden'})
def validate_email_verification(self, value):
self._token = EmailVerificationToken(value)
try:
self._token.validate()
except TokenError as err:
raise ValidationError(err.args)
user_query = User.objects.filter(email=self._token['email'])
if not user_query.exists():
raise ValidationError(
_("User with email doesn't exist"),
code='email_not_exists')
self._user = user_query.get()
if self._user.is_verified:
raise ValidationError(
_("User is already verified"),
code='user_already_verified')
return value
def verify(self):
if not hasattr(self, '_token') or not hasattr(self, '_user'):
self.is_valid(raise_exception=True)
self._user.is_verified = True
self._user.save()
class ProfileSerializer(ModelSerializer):
class Meta:
model = Profile
fields = '__all__'
class ProfilePartialSerializer(ProfileSerializer):
class Meta:
model = Profile