diff --git a/backend/acara_donor/admin.py b/backend/acara_donor/admin.py index 1236a7065d736c45016eedad6086e8c7e52cd51e..fa8f89c5b36fbf369e447ff66de9b6fbe397d8cc 100644 --- a/backend/acara_donor/admin.py +++ b/backend/acara_donor/admin.py @@ -31,14 +31,16 @@ download_acara_donor.short_description = 'Download Acara Donor as Excel Workbook @admin.register(AcaraDonor) class AcaraDonorAdmin(admin.ModelAdmin): - list_filter = ('status', AcaraDonorFilter, 'alamat_lokasi_donor') + list_filter = ('status', AcaraDonorFilter, 'kecamatan', 'kategori') + search_fields = ('waktu_mulai',) readonly_fields = ('nomor', 'user', 'nama_institusi', 'alamat_institusi', - 'no_telp_kantor', 'email_kantor', + 'no_telp_kantor', 'nama_koor', 'email_koor', - 'no_telp_koor') - actions = [download_acara_donor] + 'no_telp_koor', + 'keterangan', + 'foto_lokasi') diff --git a/backend/acara_donor/factories.py b/backend/acara_donor/factories.py index 05d6cfc776b317a18b679c34a4a3dcfcd6331e1d..0ba19c769f2ac4369b28aaf593b41116e533aa8f 100644 --- a/backend/acara_donor/factories.py +++ b/backend/acara_donor/factories.py @@ -1,6 +1,9 @@ import factory -import dateutil.tz +from datetime import timedelta +from django.utils import timezone +from random import choice, randint from acara_donor.models import AcaraDonor +from donor.models import JadwalDonor from main.factories import UserFactory @@ -14,14 +17,27 @@ class AcaraDonorFactory(factory.DjangoModelFactory): nomor = factory.Faker('uuid4') user = factory.SubFactory(UserFactory) status = factory.Faker('boolean', chance_of_getting_true=66) + nama_institusi = factory.Faker('company') alamat_institusi = factory.Faker('address', locale=LOCALE) - alamat_lokasi_donor = factory.Faker('address', locale=LOCALE) - no_telp_kantor = factory.Faker('phone_number') email_kantor = factory.Faker('company_email', locale=LOCALE) + no_telp_kantor = factory.Faker('phone_number') + nama_koor = factory.Faker('name', locale=LOCALE) email_koor = factory.Faker('free_email', locale=LOCALE) no_telp_koor = factory.Faker('phone_number') - waktu_donor = factory.Faker('future_datetime', tzinfo=dateutil.tz.gettz('Asia/Jakarta')) + + kategori = factory.LazyAttribute(lambda _: + choice(JadwalDonor.Category.choices)[0]) + alamat_lokasi_donor = factory.Faker('address', locale=LOCALE) + kecamatan = factory.LazyAttribute(lambda _: + choice(JadwalDonor.Kecamatan.choices)[0]) + waktu_mulai = factory.LazyAttribute(lambda _: timezone.now() + + timedelta( + days=randint(1, 66), + hours=randint(1, 23))) # NOSONAR + waktu_berakhir = factory.LazyAttribute(lambda t: t.waktu_mulai + + timedelta( + hours=randint(2, 6))) # NOSONAR perkiraan_jumlah_donor = factory.fuzzy.FuzzyInteger(low=33, high=666) keterangan = factory.Faker('text') diff --git a/backend/acara_donor/filters.py b/backend/acara_donor/filters.py index 1472b126dcb8333d0c27a3f05f0bda224676d5d1..32c967f448c1487b42c14e045fd1d6489f4bd4e7 100644 --- a/backend/acara_donor/filters.py +++ b/backend/acara_donor/filters.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ class AcaraDonorFilter(admin.SimpleListFilter): title = _('waktu donor') - parameter_name = 'waktu_donor' + parameter_name = 'waktu_mulai' def lookups(self, request, model_admin): return ( @@ -24,33 +24,34 @@ class AcaraDonorFilter(admin.SimpleListFilter): def __last_day_of_month(self, any_day): next_month = any_day.replace(day=28) + datetime.timedelta(days=4) - return next_month - datetime.timedelta(days=next_month.day) + datetime.timedelta(days=1) + return next_month - datetime.timedelta(days=next_month.day) \ + + datetime.timedelta(days=1) def queryset(self, request, queryset): - today = localtime(now()).replace(hour=0, minute=0, second=0, microsecond=0) - tomorrow = today + datetime.timedelta(days=1) + today = localtime(now()).replace( + hour=0, minute=0, second=0, microsecond=0) next_seven_days = today + datetime.timedelta(days=8) this_month = today.replace(day=1) end_of_this_month = self.__last_day_of_month(today) - start_of_next_month = today.replace(year=today.year, month=today.month + 1, day=1) + start_of_next_month = today.replace(year=today.year, + month=today.month + 1, day=1) end_of_next_month = self.__last_day_of_month(start_of_next_month) - this_year = today.replace(month=1, day=1) - next_year = today.replace(year=today.year + 1, month=1, day=1) - if self.value() == 'today': - return queryset.filter(waktu_donor__range=[today, tomorrow]) + return queryset.filter(waktu_mulai__date=today) if self.value() == 'next_seven_days': - return queryset.filter(waktu_donor__range=[today, next_seven_days]) + return queryset.filter(waktu_mulai__range=[today, next_seven_days]) if self.value() == 'this_month': - return queryset.filter(waktu_donor__range=[this_month, end_of_this_month]) + return queryset.filter( + waktu_mulai__range=[this_month, end_of_this_month]) if self.value() == 'next_month': - return queryset.filter(waktu_donor__range=[start_of_next_month, end_of_next_month]) + return queryset.filter(waktu_mulai__range=[start_of_next_month, + end_of_next_month]) if self.value() == 'this_year': - return queryset.filter(waktu_donor__range=[this_year, next_year]) + return queryset.filter(waktu_mulai__year=today.year) diff --git a/backend/acara_donor/management/commands/acara_donor_seeder.py b/backend/acara_donor/management/commands/acara_donor_seeder.py index 2a25439acb0bbb80aee4d920a46786f16c3e4502..f41f3a0b9aafac7c8154559bfa042a9e51d91970 100644 --- a/backend/acara_donor/management/commands/acara_donor_seeder.py +++ b/backend/acara_donor/management/commands/acara_donor_seeder.py @@ -1,19 +1,35 @@ import random -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError +from main.models import User from acara_donor.factories import AcaraDonorFactory from main.factories import UserFactory class Command(BaseCommand): - help = 'Seeds the database.' + help = 'Seeds the database with dummy acara donor.' + + def add_arguments(self, parser): + parser.add_argument('--users_emails', nargs='+', type=str) def handle(self, *args, **options): list_user = [] - for _ in range(33): - user = UserFactory() - user.save() - list_user.append(user) - for _ in range(123): - i = random.randint(0, 32) # NOSONAR - AcaraDonorFactory(user=list_user[i]) - self.stdout.write(self.style.SUCCESS("Successfully seeds the database with dummy acara donor.")) + if not options['users_emails']: + for _ in range(33): + user = UserFactory() + user.save() + list_user.append(user) + else: + for email in options['users_emails']: + try: + list_user.append(User.objects.get(email=email)) + except User.DoesNotExist: + raise CommandError(f"User {email} does not exist.") + + for i in range(len(list_user)): + amount = random.randint(0, 32) # NOSONAR + for _ in range(amount): + AcaraDonorFactory(user=list_user[i]) + + self.stdout.write( + self.style.SUCCESS( + "Successfully seeds the database with dummy acara donor.")) diff --git a/backend/acara_donor/migrations/0001_initial.py b/backend/acara_donor/migrations/0001_initial.py index 8fac6bd48235f13e2c542615b8e658c2d17d69d8..959b4e183c7be08c055af49abbf4643184f1a910 100644 --- a/backend/acara_donor/migrations/0001_initial.py +++ b/backend/acara_donor/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.5 on 2020-05-04 08:29 +# Generated by Django 3.0.5 on 2020-06-01 19:27 from django.conf import settings import django.core.validators @@ -28,14 +28,18 @@ class Migration(migrations.Migration): ('nama_koor', models.CharField(max_length=70)), ('email_koor', models.EmailField(max_length=254)), ('no_telp_koor', models.CharField(max_length=20)), + ('kategori', models.CharField(choices=[('Terbuka', 'Public'), ('Tertutup', 'Private')], max_length=20)), ('alamat_lokasi_donor', models.CharField(max_length=140)), - ('waktu_donor', models.DateTimeField()), + ('kecamatan', models.CharField(choices=[('Beji', 'Beji'), ('Pancoran Mas', 'Pancoran Mas'), ('Cipayung', 'Cipayung'), ('Sukmajaya', 'Sukmajaya'), ('Cilodong', 'Cilodong'), ('Limo', 'Limo'), ('Cinere', 'Cinere'), ('Cimanggis', 'Cimanggis'), ('Tapos', 'Tapos'), ('Sawangan', 'Sawangan'), ('Bojong Sari', 'Bojong Sari')], max_length=20)), + ('waktu_mulai', models.DateTimeField()), + ('waktu_berakhir', models.DateTimeField()), ('perkiraan_jumlah_donor', models.IntegerField(validators=[django.core.validators.MinValueValidator(11)])), ('keterangan', models.TextField(blank=True)), + ('foto_lokasi', models.ImageField(blank=True, upload_to='images')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ - 'ordering': ['waktu_donor'], + 'ordering': ['-waktu_mulai'], }, ), ] diff --git a/backend/acara_donor/migrations/0002_auto_20200505_0332.py b/backend/acara_donor/migrations/0002_auto_20200505_0332.py deleted file mode 100644 index 6cb1ad5a0dd44237fec176590b3e5fec182062ce..0000000000000000000000000000000000000000 --- a/backend/acara_donor/migrations/0002_auto_20200505_0332.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.0.5 on 2020-05-04 20:32 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('acara_donor', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='acaradonor', - options={'ordering': ['-waktu_donor']}, - ), - ] diff --git a/backend/acara_donor/models.py b/backend/acara_donor/models.py index 727669cd16df82a44ad9e8b86f54893120c99eaa..85f3a9a668506726c34b8b5dbc5f959bab613f28 100644 --- a/backend/acara_donor/models.py +++ b/backend/acara_donor/models.py @@ -1,6 +1,8 @@ from django.db import models from django.core.validators import MinValueValidator +from django.utils.timezone import localtime from main.models import User +from donor.models import JadwalDonor import uuid @@ -8,21 +10,35 @@ class AcaraDonor(models.Model): nomor = models.UUIDField(primary_key=True, default=uuid.uuid4) user = models.ForeignKey(User, on_delete=models.CASCADE) status = models.BooleanField(null=True) + nama_institusi = models.CharField(max_length=95) alamat_institusi = models.CharField(max_length=140) email_kantor = models.EmailField() no_telp_kantor = models.CharField(max_length=20) + nama_koor = models.CharField(max_length=70) email_koor = models.EmailField() no_telp_koor = models.CharField(max_length=20) + + kategori = models.CharField(max_length=20, + choices=JadwalDonor.Category.choices) alamat_lokasi_donor = models.CharField(max_length=140) - waktu_donor = models.DateTimeField() - perkiraan_jumlah_donor = models.IntegerField(validators=[MinValueValidator(11)]) + kecamatan = models.CharField(max_length=20, + choices=JadwalDonor.Kecamatan.choices) + waktu_mulai = models.DateTimeField() + waktu_berakhir = models.DateTimeField() + perkiraan_jumlah_donor = models.IntegerField( + validators=[MinValueValidator(11)]) keterangan = models.TextField(blank=True) + foto_lokasi = models.ImageField(upload_to='images', blank=True) def __str__(self): - waktu_donor = self.waktu_donor.strftime("%d/%m/%Y, %H:%M") - return f"{self.nama_institusi}: {self.alamat_lokasi_donor}, {waktu_donor}" + waktu_mulai = localtime(self.waktu_mulai) + waktu_berakhir = localtime(self.waktu_berakhir) + return f"{self.nama_institusi}: {self.alamat_lokasi_donor}, \ + {waktu_mulai.date()} \ + ({waktu_mulai.hour:02d}:{waktu_mulai.minute:02d} \ + - {waktu_berakhir.hour:02d}:{waktu_berakhir.minute:02d})" class Meta: - ordering = ['-waktu_donor'] + ordering = ['-waktu_mulai'] diff --git a/backend/acara_donor/serializers.py b/backend/acara_donor/serializers.py index 6b26383635d60dc7c1e94dfca30eb785d07fa2be..330caeb4cd629be29f44b6578429aa79fa20e27a 100644 --- a/backend/acara_donor/serializers.py +++ b/backend/acara_donor/serializers.py @@ -5,15 +5,25 @@ from acara_donor.models import AcaraDonor class AcaraDonorSerializer(serializers.ModelSerializer): def validate(self, data): - queryset = AcaraDonor.objects.filter(nama_institusi=data['nama_institusi'], - alamat_lokasi_donor=data['alamat_lokasi_donor'], - waktu_donor=data['waktu_donor']) + queryset = AcaraDonor.objects.filter( + nama_institusi=data['nama_institusi'], + alamat_lokasi_donor=data['alamat_lokasi_donor'], + waktu_mulai=data['waktu_mulai']) if queryset.exists(): - raise serializers.ValidationError("Acara donor darah ini sudah diajukan.") - if data['waktu_donor'] < timezone.now(): - raise serializers.ValidationError("Tanggal acara donor darah tidak boleh di masa lalu.") + raise serializers.ValidationError( + "Acara donor darah ini sudah diajukan.") + + if data['waktu_mulai'] < timezone.now(): + raise serializers.ValidationError( + "Pelaksanaan acara donor darah tidak boleh di masa lalu.") + + if data['waktu_berakhir'] < data['waktu_mulai']: + raise serializers.ValidationError( + "Jam pelaksanaan acara donor darah tidak valid.") + return data class Meta: model = AcaraDonor - exclude = ["user", "status"] + exclude = ["user", "nomor", "status"] + extra_kwargs = {'foto_lokasi': {'required': False}} diff --git a/backend/acara_donor/test_commands.py b/backend/acara_donor/test_commands.py index c25cfa4a92b4c4aaad473660026e9c133451643b..5eb15c9f8866c35536968673d0193ddfcfa3faaf 100644 --- a/backend/acara_donor/test_commands.py +++ b/backend/acara_donor/test_commands.py @@ -1,7 +1,8 @@ from io import StringIO -from django.core.management import call_command +from django.core.management import call_command, CommandError from django.test import TestCase +from main.factories import UserFactory class SeedAcaraDonorTest(TestCase): @@ -9,3 +10,22 @@ class SeedAcaraDonorTest(TestCase): out = StringIO() call_command('acara_donor_seeder', stdout=out) self.assertIn('Success', out.getvalue()) + + def test_command_with_email_provided_success(self): + user1 = UserFactory(email="tes1@gmail.com") + user2 = UserFactory(email="tes2@gmail.com") + user1.save() + user2.save() + out = StringIO() + call_command( + 'acara_donor_seeder', users_emails=["tes1@gmail.com", + "tes2@gmail.com"], + stdout=out) + self.assertIn('Success', out.getvalue()) + + def test_command_with_email_provided_failed(self): + out = StringIO() + with self.assertRaisesMessage(CommandError, + 'User bities@gmail.com does not exist.'): + call_command('acara_donor_seeder', + users_emails=["bities@gmail.com", ], stdout=out) diff --git a/backend/acara_donor/tests.py b/backend/acara_donor/tests.py index f81b4c78cb6dfb6eecc2549881c98a80148fc5ae..8ac71bccc85cfdc4319eaf8acda8cbd4006fd601 100644 --- a/backend/acara_donor/tests.py +++ b/backend/acara_donor/tests.py @@ -2,8 +2,8 @@ import datetime from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase -from django.urls import reverse from django.utils import timezone +from django.utils.timezone import localtime from rest_framework import status from rest_framework.test import APITestCase from rest_framework_authlib.tokens import AccessToken @@ -24,13 +24,18 @@ class AcaraDonorTest(TestCase): user = UserFactory() user.save() acara_donor = AcaraDonorFactory(user=user) - waktu_donor = acara_donor.waktu_donor.strftime("%d/%m/%Y, %H:%M") + waktu_mulai = localtime(acara_donor.waktu_mulai) + waktu_berakhir = localtime(acara_donor.waktu_berakhir) self.assertTrue(isinstance(acara_donor, AcaraDonor)) - self.assertEqual((str(acara_donor.nama_institusi) + ': ' + - str(acara_donor.alamat_lokasi_donor) + ", " + waktu_donor), + self.assertEqual((f"{acara_donor.nama_institusi}: {acara_donor.alamat_lokasi_donor}, \ + {waktu_mulai.date()} \ + ({waktu_mulai.hour:02d}:{waktu_mulai.minute:02d} \ + - {waktu_berakhir.hour:02d}:{waktu_berakhir.minute:02d})"), str(acara_donor)) +@patch('django.utils.timezone.now', return_value=datetime.datetime( + 2020, 6, 12, 9, 0, tzinfo=datetime.timezone.utc)) class AcaraDonorSerializerTest(TestCase): """ Test module for Acara Donor serializer""" @@ -38,62 +43,94 @@ class AcaraDonorSerializerTest(TestCase): self.user = UserFactory() self.user.save() self.acara_donor = AcaraDonorFactory(user=self.user) + base_time = timezone.localtime( + datetime.datetime(2020, 6, 12, 9, 18, + tzinfo=datetime.timezone.utc)) self.acara_donor_attr = {"alamat_institusi": "Pacilkom", "alamat_lokasi_donor": "Sekre Pacil", "email_kantor": "pacil@cs.ui.ac.id", "email_koor": "pacilia@gmail.com", + "kategori": "Terbuka", + "kecamatan": "Beji", "keterangan": "", "nama_institusi": "Pacilkom", "nama_koor": "dr. Pacilia", "no_telp_kantor": "08165342342", "no_telp_koor": "08167021743", - "nomor": "ea582779-fdfc-444e-ad03-4084e715af0a", "perkiraan_jumlah_donor": 455, - "waktu_donor": timezone.now() + timezone.timedelta(days=3)} + "waktu_berakhir": base_time + + timezone.timedelta(hours=5), + "waktu_mulai": base_time + + timezone.timedelta(hours=3)} + + def test_create_acara_donor_failed_due_to_duplication(self, + mock_timezone): + serializer = AcaraDonorSerializer(instance=self.acara_donor, + data=self.acara_donor_attr) - def test_create_acara_donor_failed_due_to_duplication(self): - serializer = AcaraDonorSerializer(instance=self.acara_donor, data=self.acara_donor_attr) serializer.is_valid() new_acara_donor = serializer.save() new_acara_donor.refresh_from_db() - serializer = AcaraDonorSerializer(instance=self.acara_donor, data=self.acara_donor_attr) + serializer = AcaraDonorSerializer(instance=self.acara_donor, + data=self.acara_donor_attr) + self.assertFalse(serializer.is_valid()) + + def test_create_acara_donor_failed_because_time_has_passed(self, + mock_timezone): + self.acara_donor_attr['waktu_mulai'] = timezone.now() \ + - timezone.timedelta(days=3) + serializer = AcaraDonorSerializer(instance=self.acara_donor, + data=self.acara_donor_attr) self.assertFalse(serializer.is_valid()) - def test_create_acara_donor_failed_because_time_has_passed(self): - self.acara_donor_attr['waktu_donor'] = timezone.now() - timezone.timedelta(days=3) - serializer = AcaraDonorSerializer(instance=self.acara_donor, data=self.acara_donor_attr) + def test_create_acara_donor_failed_because_end_time_is_before_start_time( + self, mock_timezone): + self.acara_donor_attr['waktu_mulai'] = timezone.now() \ + + timezone.timedelta(hours=5) + self.acara_donor_attr['waktu_berakhir'] = timezone.now() \ + + timezone.timedelta(hours=3) + serializer = AcaraDonorSerializer(instance=self.acara_donor, + data=self.acara_donor_attr) self.assertFalse(serializer.is_valid()) +@patch('django.utils.timezone.now', return_value=datetime.datetime( + 2020, 6, 12, 9, 0, tzinfo=datetime.timezone.utc)) class AcaraDonorCreateViewTest(APITestCase): """ Test module for Acara Donor API""" def setUp(self): self.user = UserFactory() self.user.save() - self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + str(AccessToken.for_user(self.user))) + self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + + str(AccessToken.for_user(self.user))) - def test_create_acara_donor(self): + def test_create_acara_donor_with_post_request(self, mock_timezone): response = self.client.post('/acara-donor/pengajuan/', {"alamat_institusi": "Pacilkom", "alamat_lokasi_donor": "Sekre Pacil", "email_kantor": "pacil@cs.ui.ac.id", "email_koor": "pacilia@gmail.com", + "foto_lokasi": "", + "kategori": "Terbuka", + "kecamatan": "Beji", "keterangan": "", "nama_institusi": "Pacilkom", "nama_koor": "dr. Pacilia", "no_telp_kantor": "08165342342", "no_telp_koor": "08167021743", - "nomor": "ea582779-fdfc-444e-ad03-4084e715af0a", "perkiraan_jumlah_donor": 455, - "waktu_donor": timezone.now() + timezone.timedelta(days=3)}, - format='json') + "waktu_berakhir": timezone.now() + + timezone.timedelta(hours=7), + "waktu_mulai": timezone.now() + + timezone.timedelta(hours=5)}, + format='multipart') self.assertEqual(response.status_code, status.HTTP_201_CREATED) -@patch('acara_donor.filters.now', - new=lambda: datetime.datetime(2020, 5, 10, 9, 0, tzinfo=datetime.timezone.utc)) +@patch('acara_donor.filters.now', new=lambda: + datetime.datetime(2020, 5, 10, 9, 0, tzinfo=datetime.timezone.utc)) class AcaraDonorFilterTest(TestCase): """ Test module for Acara Donor filter""" @@ -101,63 +138,61 @@ class AcaraDonorFilterTest(TestCase): self.user = UserFactory() self.user.save() - base_time = timezone.localtime(datetime.datetime(2020, 5, 10, 9, 14, - tzinfo=datetime.timezone.utc)) + base_time = timezone.localtime(datetime.datetime( + 2020, 5, 10, 9, 14, tzinfo=datetime.timezone.utc)) - self.acara_donor_today = AcaraDonorFactory(user=self.user, - waktu_donor=base_time + timezone.timedelta(hours=3)) + self.acara_donor_today = AcaraDonorFactory( + user=self.user, + waktu_mulai=base_time + timezone.timedelta(hours=3)) self.acara_donor_today.save() - self.acara_donor_overmorrow = AcaraDonorFactory(user=self.user, - waktu_donor=base_time + timezone.timedelta(days=2)) + self.acara_donor_overmorrow = AcaraDonorFactory( + user=self.user, + waktu_mulai=base_time + timezone.timedelta(days=2)) self.acara_donor_overmorrow.save() - self.acara_donor_next_month = AcaraDonorFactory(user=self.user, - waktu_donor=base_time + timezone.timedelta(days=33)) + self.acara_donor_next_month = AcaraDonorFactory( + user=self.user, + waktu_mulai=base_time + timezone.timedelta(days=33)) self.acara_donor_next_month.save() def test_filter_acara_donor_today_success(self): - filter_today = AcaraDonorFilter(None, {'waktu_donor': 'today'}, AcaraDonor, AcaraDonorAdmin) - acara_donor_filtered = filter_today.queryset(None, AcaraDonor.objects.all())[0] - self.assertEqual(acara_donor_filtered.waktu_donor, self.acara_donor_today.waktu_donor) + filter_today = AcaraDonorFilter(None, {'waktu_mulai': 'today'}, + AcaraDonor, AcaraDonorAdmin) + acara_donor_filtered = filter_today.queryset( + None, AcaraDonor.objects.all())[0] + self.assertEqual(acara_donor_filtered.waktu_mulai, + self.acara_donor_today.waktu_mulai) def test_filter_acara_donor_next_seven_days_success(self): - filter_seven_days = AcaraDonorFilter(None, {'waktu_donor': 'next_seven_days'}, AcaraDonor, AcaraDonorAdmin) - acara_donor_filtered = filter_seven_days.queryset(None, AcaraDonor.objects.all())[0] - self.assertEqual(acara_donor_filtered.waktu_donor, self.acara_donor_overmorrow.waktu_donor) + filter_seven_days = AcaraDonorFilter( + None, {'waktu_mulai': 'next_seven_days'}, + AcaraDonor, AcaraDonorAdmin) + acara_donor_filtered = filter_seven_days.queryset( + None, AcaraDonor.objects.all())[0] + self.assertEqual(acara_donor_filtered.waktu_mulai, + self.acara_donor_overmorrow.waktu_mulai) def test_filter_acara_donor_this_month_success(self): - filter_this_month = AcaraDonorFilter(None, {'waktu_donor': 'this_month'}, AcaraDonor, AcaraDonorAdmin) - acara_donor_filtered = filter_this_month.queryset(None, AcaraDonor.objects.all())[0] - self.assertEqual(acara_donor_filtered.waktu_donor, self.acara_donor_overmorrow.waktu_donor) + filter_this_month = AcaraDonorFilter( + None, {'waktu_mulai': 'this_month'}, AcaraDonor, AcaraDonorAdmin) + acara_donor_filtered = filter_this_month.queryset( + None, AcaraDonor.objects.all())[0] + self.assertEqual(acara_donor_filtered.waktu_mulai, + self.acara_donor_overmorrow.waktu_mulai) def test_filter_acara_donor_next_month_success(self): - filter_next_month = AcaraDonorFilter(None, {'waktu_donor': 'next_month'}, AcaraDonor, AcaraDonorAdmin) - acara_donor_filtered = filter_next_month.queryset(None, AcaraDonor.objects.all())[0] - self.assertEqual(acara_donor_filtered.waktu_donor, self.acara_donor_next_month.waktu_donor) + filter_next_month = AcaraDonorFilter( + None, {'waktu_mulai': 'next_month'}, AcaraDonor, AcaraDonorAdmin) + acara_donor_filtered = filter_next_month.queryset( + None, AcaraDonor.objects.all())[0] + self.assertEqual(acara_donor_filtered.waktu_mulai, + self.acara_donor_next_month.waktu_mulai) def test_filter_acara_donor_this_year_success(self): - filter_this_year = AcaraDonorFilter(None, {'waktu_donor': 'this_year'}, AcaraDonor, AcaraDonorAdmin) - acara_donor_filtered = filter_this_year.queryset(None, AcaraDonor.objects.all())[0] - self.assertEqual(acara_donor_filtered.waktu_donor, self.acara_donor_next_month.waktu_donor) - - -class AcaraDonorAdminTest(TestCase): - - def setUp(self): - username = 'admin@host' - password = 'secretsauce' - User.objects.create_superuser(username, password) - self.client.login(username=username, password=password) - - def test_action_download_acaradonor(self): - user = UserFactory() - user.save() - acara_donor = AcaraDonorFactory(user=user) - acara_donor.save() - - endpoint = reverse('admin:acara_donor_acaradonor_changelist') - data = {'action': 'download_acara_donor', '_selected_action': [acara_donor.nomor]} - response = self.client.post(endpoint, data=data) - - self.assertEqual(response['content-type'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + filter_this_year = AcaraDonorFilter(None, {'waktu_mulai': 'this_year'}, + AcaraDonor, AcaraDonorAdmin) + acara_donor_filtered = filter_this_year.queryset( + None, AcaraDonor.objects.all())[0] + self.assertEqual(acara_donor_filtered.waktu_mulai, + self.acara_donor_next_month.waktu_mulai) diff --git a/backend/donor/migrations/0011_auto_20200602_0227.py b/backend/donor/migrations/0011_auto_20200602_0227.py new file mode 100644 index 0000000000000000000000000000000000000000..305090c089e8d0f0d5587d810287d4b6f3fcdfa0 --- /dev/null +++ b/backend/donor/migrations/0011_auto_20200602_0227.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-06-01 19:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('donor', '0010_auto_20200427_0713'), + ] + + operations = [ + migrations.AlterField( + model_name='jadwaldonor', + name='category', + field=models.CharField(choices=[('Terbuka', 'Public'), ('Tertutup', 'Private')], max_length=20), + ), + ] diff --git a/backend/donor/models.py b/backend/donor/models.py index 5b15049d89ef0843fbf7813b38add0d1d3262f72..f176abd0e0a7cf996b0857899dee6c2e76eb6a4d 100644 --- a/backend/donor/models.py +++ b/backend/donor/models.py @@ -22,8 +22,8 @@ class JadwalDonor(models.Model): BOJONG_SARI = "Bojong Sari" class Category(models.TextChoices): - PUBLIC = "terbuka" - PRIVATE = "tertutup" + PUBLIC = "Terbuka" + PRIVATE = "Tertutup" kecamatan = models.CharField(max_length=20, choices=Kecamatan.choices) location = models.CharField(max_length=30) diff --git a/backend/media/formulir-daftar-donor/formulir-1.pdf b/backend/media/formulir-daftar-donor/formulir-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..654d9af2b97e71639cf2ca967165b0f29e41c13f Binary files /dev/null and b/backend/media/formulir-daftar-donor/formulir-1.pdf differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4e75eb0ef8b8f36c505b9ee43e3987f605d7a00b..b3dd144a8f47c9cc3efd06bccae011935e53e320 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1041,6 +1041,24 @@ "minimist": "^1.2.0" } }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/moment": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz", + "integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.27", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.27.tgz", @@ -1510,6 +1528,124 @@ } } }, + "@material-ui/core": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.13.tgz", + "integrity": "sha512-GEXNwUr+laZ0N+F1efmHB64Fyg+uQIRXLqbSejg3ebSXgLYNpIjnMOPRfWdu4rICq0dAIgvvNXGkKDMcf3AMpA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/react-transition-group": "^4.3.0", + "@material-ui/styles": "^4.9.13", + "@material-ui/system": "^4.9.13", + "@material-ui/types": "^5.0.1", + "@material-ui/utils": "^4.9.12", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "^1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0", + "react-transition-group": "^4.3.0" + } + }, + "@material-ui/pickers": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", + "integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==", + "requires": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + } + }, + "@material-ui/react-transition-group": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@material-ui/react-transition-group/-/react-transition-group-4.3.0.tgz", + "integrity": "sha512-CwQ0aXrlUynUTY6sh3UvKuvye1o92en20VGAs6TORnSxUYeRmkX8YeTUN3lAkGiBX1z222FxLFO36WWh6q73rQ==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "dependencies": { + "dom-helpers": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", + "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^2.6.7" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", + "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + } + } + }, + "@material-ui/styles": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.13.tgz", + "integrity": "sha512-lWlXJanBdHQ18jW/yphedRokHcvZD1GdGzUF/wQxKDsHwDDfO45ZkAxuSBI202dG+r1Ph483Z3pFykO2obeSRA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.0.1", + "@material-ui/utils": "^4.9.6", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + } + }, + "@material-ui/system": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.13.tgz", + "integrity": "sha512-6AlpvdW6KJJ5bF1Xo2OD13sCN8k+nlL36412/bWnWZOKIfIMo/Lb8c8d1DOIaT/RKWxTEUaWnKZjabVnA3eZjA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.9.6", + "prop-types": "^15.7.2" + } + }, + "@material-ui/types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.1.tgz", + "integrity": "sha512-wURPSY7/3+MAtng3i26g+WKwwNE3HEeqa/trDBR5+zWKmcjO+u9t7Npu/J1r+3dmIa/OeziN9D/18IrBKvKffw==" + }, + "@material-ui/utils": { + "version": "4.9.12", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.9.12.tgz", + "integrity": "sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ==", + "requires": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0" + } + }, "@mikaelkristiansson/domready": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/@mikaelkristiansson/domready/-/domready-1.0.10.tgz", @@ -1966,12 +2102,28 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz", + "integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==", + "requires": { + "@types/react": "*" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/styled-jsx": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz", + "integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==", + "requires": { + "@types/react": "*" + } + }, "@types/testing-library__dom": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-7.0.1.tgz", @@ -4193,6 +4345,11 @@ "mimic-response": "^1.0.0" } }, + "clsx": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz", + "integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4833,6 +4990,15 @@ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=" }, + "css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "requires": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "css-what": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", @@ -10202,6 +10368,11 @@ "is-extglob": "^2.1.1" } }, + "is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, "is-installed-globally": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", @@ -13773,6 +13944,83 @@ "verror": "1.10.0" } }, + "jss": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.1.1.tgz", + "integrity": "sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "csstype": "^2.6.5", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-camel-case": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz", + "integrity": "sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw==", + "requires": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.1.1" + } + }, + "jss-plugin-default-unit": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz", + "integrity": "sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.1.1" + } + }, + "jss-plugin-global": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz", + "integrity": "sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.1.1" + } + }, + "jss-plugin-nested": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz", + "integrity": "sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.1.1", + "tiny-warning": "^1.0.2" + } + }, + "jss-plugin-props-sort": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz", + "integrity": "sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.1.1" + } + }, + "jss-plugin-rule-value-function": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz", + "integrity": "sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw==", + "requires": { + "@babel/runtime": "^7.3.1", + "jss": "10.1.1" + } + }, + "jss-plugin-vendor-prefixer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz", + "integrity": "sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw==", + "requires": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.7", + "jss": "10.1.1" + } + }, "jsx-ast-utils": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", @@ -17068,6 +17316,14 @@ } } }, + "react-dropzone-uploader": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/react-dropzone-uploader/-/react-dropzone-uploader-2.11.0.tgz", + "integrity": "sha512-1DpdPMGKP7vYL5SeCh13HCl+Xrz0F6jGrDPU5Tj2ojEIXGMCtfflrZhyXdr7u40IkQ+hYjAUEEtJW24SiY8WRA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "react-error-overlay": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-3.0.0.tgz", @@ -17751,6 +18007,14 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "rimraf": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", @@ -19396,6 +19660,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 61eaa8ce7ddd55c804de99134654f3bfec802855..2969339bae036feded26f52a91f694682046b2e7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,9 +5,12 @@ "version": "0.1.0", "author": "Kyle Mathews ", "dependencies": { + "@date-io/moment": "^1.3.13", "@fortawesome/fontawesome-svg-core": "^1.2.27", "@fortawesome/free-solid-svg-icons": "^5.13.0", "@fortawesome/react-fontawesome": "^0.1.8", + "@material-ui/core": "^4.9.13", + "@material-ui/pickers": "^3.2.10", "apexcharts": "^3.18.1", "bootstrap": "^4.4.1", "gatsby": "^2.19.7", @@ -17,7 +20,7 @@ "gatsby-plugin-root-import": "^2.0.5", "gatsby-source-filesystem": "^2.1.46", "inquirer": "^6.5.2", - "moment": "2.24.0", + "moment": "^2.24.0", "prop-types": "^15.7.2", "react": "^16.13.0", "react-animated-slider": "^2.0.0", @@ -25,6 +28,7 @@ "react-bootstrap": "^1.0.0-beta.16", "react-calendar": "^2.19.2", "react-dom": "^16.13.0", + "react-dropzone-uploader": "^2.11.0", "react-google-login": "^5.1.1", "react-helmet": "^5.2.1", "react-hook-form": "^5.3.1", diff --git a/frontend/src/api.js b/frontend/src/api.js index e00ac732b4d07fd636556b8571d4512e4315de9c..c164b5a9134ad01a309217ca7182fc94e0c1167b 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,5 +1,4 @@ import axios from "axios" -import { useLocalStorage } from "react-use" import { BASE_API_URL } from "./config" axios.defaults.baseURL = BASE_API_URL diff --git a/frontend/src/components/ModalChangeProfile.js b/frontend/src/components/ModalChangeProfile.js index 1b555a914759e258909cc44586a1a8ddccbaab09..67c39d0e8dc8cf941291402e4aa79259b5626e3d 100644 --- a/frontend/src/components/ModalChangeProfile.js +++ b/frontend/src/components/ModalChangeProfile.js @@ -5,6 +5,7 @@ import { ProfileSchema } from "./form-validation-schema" import ModalCompleteProfile from "./complete-profile" import { useAuth } from "../hooks/authenticate" import { withAuthenticated } from "./authenticated-only" +import moment from "moment" const ModalChangeProfile = ({ show, handleClose }) => { const { user, getAndSetUserProfile } = useAuth() @@ -19,6 +20,8 @@ const ModalChangeProfile = ({ show, handleClose }) => { }, [reset, user]) const onSubmit = data => { + data.birthdate = moment(data.birthdate).format("YYYY-MM-DD") + putUserProfile(data) .then(() => { window.alert("Data berhasil diubah.") diff --git a/frontend/src/components/ModalEventDonorSubmission.js b/frontend/src/components/ModalEventDonorSubmission.js index 8042aaa7ee9d4d93449abec1872f9dcc6ba24c34..7e56c1df2562db58e0f7797b10c8f479c1fef2f0 100644 --- a/frontend/src/components/ModalEventDonorSubmission.js +++ b/frontend/src/components/ModalEventDonorSubmission.js @@ -1,20 +1,46 @@ -import React from "react" -import { Modal, Button, Form, Row } from "react-bootstrap" +import React, { useEffect } from "react" +import { Modal, Button, Form, Row, Col } from "react-bootstrap" import { useForm, Controller } from "react-hook-form" -import { EventSubmissionSchema } from "./form-validation-schema" +import { EventSubmissionSchema, MAX_IMAGE_SIZE } from "./form-validation-schema" import { postAjukanAcaraDonor } from "../api" +import { ThemeProvider } from "@material-ui/core/styles" +import theme from "../styles/theme" +import MomentUtils from "@date-io/moment" +import { + KeyboardDatePicker, + KeyboardTimePicker, + MuiPickersUtilsProvider, +} from "@material-ui/pickers" import PhoneInput from "react-phone-input-2" import "react-phone-input-2/lib/style.css" import "./user-form.css" +import Dropzone from "react-dropzone-uploader" +import "react-dropzone-uploader/dist/styles.css" +import "./dropzone.css" +import moment from "moment" const ModalEventDonorSubmission = ({ show, handleClose }) => { - const { control, register, handleSubmit, errors } = useForm({ + const { control, register, setValue, handleSubmit, errors } = useForm({ mode: "onChange", validationSchema: EventSubmissionSchema, }) const onSubmit = async data => { - await postAjukanAcaraDonor(data) + const tanggal_donor = moment(data.tanggal_donor).format("YYYY-MM-DD") + const jam_mulai = moment(data.jam_mulai).format("HH:mm") + const jam_selesai = moment(data.jam_selesai).format("HH:mm") + data["waktu_mulai"] = `${tanggal_donor}T${jam_mulai}` + data["waktu_berakhir"] = `${tanggal_donor}T${jam_selesai}` + delete data["tanggal_donor"] + delete data["jam_mulai"] + delete data["jam_selesai"] + + const formData = new FormData() + Object.keys(data).forEach(key => { + if (typeof data[key] != "undefined") formData.append(key, data[key]) + }) + + await postAjukanAcaraDonor(formData) .then(() => { window.alert("Pengajuan acara donor telah disimpan.") handleClose() @@ -28,372 +54,566 @@ const ModalEventDonorSubmission = ({ show, handleClose }) => { }) } + useEffect(() => { + register({ name: "foto_lokasi" }) + }, [register]) + return ( - - + + + + - -

- Formulir Pengajuan Acara Donor Darah -

+ +

+ Formulir Pengajuan Acara Donor Darah +

-
-
- Informasi Penyelenggara -
+
+
+ Informasi Penyelenggara +
- -
- -
- Nama - * -
-
-
-
- - {errors.nama_institusi && ( - - {errors.nama_institusi.message} - - )} -
-
+ +
+ +
+ Nama Institusi + * +
+
+
+
+ + {errors.nama_institusi && ( + + {errors.nama_institusi.message} + + )} +
+
- -
- -
- Alamat Institusi - * -
-
-
-
- - {errors.alamat_institusi && ( - - {errors.alamat_institusi.message} - - )} -
-
+ +
+ +
+ Alamat Institusi + * +
+
+
+
+ + {errors.alamat_institusi && ( + + {errors.alamat_institusi.message} + + )} +
+
- -
- -
- Email Kantor - * -
-
-
-
- - {errors.email_kantor && ( - - {errors.email_kantor.message} - - )} -
-
+ +
+ +
+ Email Kantor + * +
+
+
+
+ + {errors.email_kantor && ( + + {errors.email_kantor.message} + + )} +
+
- -
- -
- No. Telepon Kantor - * -
-
-
-
- +
+ +
+ No. Telepon Kantor + * +
+
+
+
+ + } + name="no_telp_kantor" + control={control} + /> + {errors.no_telp_kantor && ( + + {errors.no_telp_kantor.message} + + )} +
+ + +
+ Kontak Koordinator +
+ + +
+ +
+ Nama Koordinator + * +
+
+
+
+ - } - name="no_telp_kantor" - control={control} - /> - {errors.no_telp_kantor && ( - - {errors.no_telp_kantor.message} - - )} -
-
+ {errors.nama_koor && ( + + {errors.nama_koor.message} + + )} +
+
-
- Kontak Koordinator -
+ +
+ +
+ Email Koordinator + * +
+
+
+
+ + {errors.email_koor && ( + + {errors.email_koor.message} + + )} +
+
- -
- -
- Nama Koordinator - * -
-
-
-
- - {errors.nama_koor && ( - - {errors.nama_koor.message} - - )} -
-
+ +
+ +
+ No. Telepon Koordinator + * +
+
+
+
+ + } + name="no_telp_koor" + control={control} + /> + {errors.no_telp_koor && ( + + {errors.no_telp_koor.message} + + )} +
+
- -
- -
- Email Koordinator - * -
-
-
-
- - {errors.email_koor && ( - - {errors.email_koor.message} - - )} -
-
+
+ Informasi Acara Donor +
- -
- -
- No. Telepon Koordinator - * -
-
-
-
- +
+ +
+ Kategori + * +
+
+
+
+ + + + +
+ + + +
+ +
+ Alamat Lokasi + * +
+
+
+
+ - } - name="no_telp_koor" - control={control} - /> - {errors.no_telp_koor && ( - - {errors.no_telp_koor.message} - - )} -
-
+ {errors.alamat_lokasi_donor && ( + + {errors.alamat_lokasi_donor.message} + + )} +
+
-
- Informasi Acara Donor -
+ +
+ +
+ Kecamatan + * +
+
+
+
+ + + + + + + + + + + + + +
+
- -
- -
- Alamat Lokasi Donor - * -
-
-
-
- - {errors.alamat_lokasi_donor && ( - - {errors.alamat_lokasi_donor.message} - - )} -
-
+ +
+ +
+ Tanggal Pelaksanaan + * +
+
+
+
+ + } + name="tanggal_donor" + control={control} + /> + {errors.tanggal_donor && ( + + {errors.tanggal_donor.message} + + )} +
+
- -
- -
- Waktu Donor - * -
-
-
-
- - {errors.waktu_donor && ( - - {errors.waktu_donor.message} - - )} -
-
+ + + + +
+ Jam Mulai + * +
+
+ + } + name="jam_mulai" + control={control} + /> + {errors.jam_mulai && ( + + {errors.jam_mulai.message} + + )} +
+ - -
- -
- Perkiraan Jumlah Donor - * -
-
-
-
- - {errors.perkiraan_jumlah_donor && ( - - {errors.perkiraan_jumlah_donor.message} - - )} -
-
+ + +
+ Jam Berakhir + * +
+
+
+ + } + name="jam_selesai" + control={control} + /> + {errors.jam_selesai && ( + + {errors.jam_selesai.message} + + )} +
+ +
- -
- -
Keterangan
-
-
-
- -
-
+ +
+ +
+ Perkiraan Jumlah Donor + * +
+
+
+
+ + {errors.perkiraan_jumlah_donor && ( + + {errors.perkiraan_jumlah_donor.message} + + )} +
+
-
- -
-
- - + +
+ +
Keterangan
+
+
+
+ +
+
+ + +
+ +
Foto Lokasi
+
+
+
+ + setValue("foto_lokasi", file) + } + accept="image/*" + maxSizeBytes={MAX_IMAGE_SIZE} + inputContent="Seret dan Lepas atau Tekan untuk Memilih Sebuah Foto" + inputProps={{ id: "formImg" }} + inputRef={register} + /> + } + name="foto_lokasi" + control={control} + /> + {errors.foto_lokasi && ( + + {errors.foto_lokasi.message} + + )} +
+
+ +
+ +
+
+
+
+
+
) } export default ModalEventDonorSubmission diff --git a/frontend/src/components/ModalEventDonorSubmissionHelper.js b/frontend/src/components/ModalEventDonorSubmissionHelper.js index 43f16dbcb6bb667dead72f5f8598382b84604a57..a6e73dca24378afd5ec467c61375bbfdfc9490fd 100644 --- a/frontend/src/components/ModalEventDonorSubmissionHelper.js +++ b/frontend/src/components/ModalEventDonorSubmissionHelper.js @@ -1,10 +1,10 @@ import { fireEvent, screen } from "@testing-library/react" +import moment from "moment" export const fillEventDonorSubmission = async () => { expect(await screen.getByLabelText(/Alamat Institusi/i)).toBeInTheDocument() - expect( - await screen.getByLabelText(/Alamat Lokasi Donor/i) - ).toBeInTheDocument() + expect(await screen.getByText(/Informasi Acara Donor/i)).toBeInTheDocument() + fireEvent.change(screen.getByPlaceholderText(/Nama Institusi\/Organisasi/i), { target: { value: "Pacilkom", @@ -25,6 +25,7 @@ export const fillEventDonorSubmission = async () => { value: "+6281234535434", }, }) + fireEvent.change(screen.getByLabelText(/Nama Koordinator/i), { target: { value: "M. Fairuzi", @@ -40,14 +41,44 @@ export const fillEventDonorSubmission = async () => { value: "+6281234535434", }, }) - fireEvent.change(screen.getByLabelText(/Alamat Lokasi Donor/i), { + + fireEvent.change(screen.getByLabelText(/Kategori/i), { + target: { + value: "Terbuka", + }, + }) + fireEvent.change(screen.getByLabelText(/Alamat Lokasi/i), { target: { value: "Pacilkom", }, }) - fireEvent.change(screen.getByLabelText(/Waktu Donor/i), { + fireEvent.change(screen.getByLabelText(/Kecamatan/i), { + target: { + value: "Beji", + }, + }) + fireEvent.change(screen.getByLabelText(/Tanggal Pelaksanaan/i), { + target: { + value: moment() + .endOf("day") + .add(1, "seconds") + .format("DD/MM/YYYY"), + }, + }) + fireEvent.change(screen.getByLabelText(/Jam Mulai/i), { + target: { + value: moment() + .endOf("day") + .add(5, "hours") + .format("HH:mm"), + }, + }) + fireEvent.change(screen.getByLabelText(/Jam Berakhir/i), { target: { - value: "2040-05-07T12:23", + value: moment() + .endOf("day") + .add(7, "hours") + .format("HH:mm"), }, }) fireEvent.change(screen.getByLabelText(/Perkiraan Jumlah Donor/i), { @@ -55,4 +86,12 @@ export const fillEventDonorSubmission = async () => { value: "666", }, }) + fireEvent.change( + screen.getByText("Seret dan Lepas atau Tekan untuk Memilih Sebuah Foto"), + { + target: { + files: [new File(["/(≧ x ≦)\"], "foto.png", { type: "image/png" })], + }, + } + ) } diff --git a/frontend/src/components/ModalGoogleCompleteProfile.js b/frontend/src/components/ModalGoogleCompleteProfile.js index 1f2bfeb2044901bc718be1344c85c94cfd5efb9e..d93df97bc0f6a0d4d0c4b0f1441972ee67df0970 100644 --- a/frontend/src/components/ModalGoogleCompleteProfile.js +++ b/frontend/src/components/ModalGoogleCompleteProfile.js @@ -5,6 +5,7 @@ import { postUserProfile } from "../api" import { ProfileSchema } from "./form-validation-schema" import ModalCompleteProfile from "./complete-profile" import { withAuthenticated } from "./authenticated-only" +import moment from "moment" const ModalGoogleCompleteProfile = ({ show, handleClose }) => { const { user, logout, getAndSetUserProfile } = useAuth() @@ -20,6 +21,8 @@ const ModalGoogleCompleteProfile = ({ show, handleClose }) => { }) const onSubmit = async data => { + data.birthdate = moment(data.birthdate).format("YYYY-MM-DD") + await postUserProfile(data) .then(() => { window.alert("Profil Anda sudah disimpan.") diff --git a/frontend/src/components/ModalRegister.js b/frontend/src/components/ModalRegister.js index 199507503e7782378c72c9ed3a2bbf6b42163101..4428b57bb7d1fc49680089dec1f6a19d350dcc06 100644 --- a/frontend/src/components/ModalRegister.js +++ b/frontend/src/components/ModalRegister.js @@ -4,6 +4,7 @@ import { postRegisterFull } from "../api" import { RegisterSchema } from "./form-validation-schema" import ModalCompleteProfile from "./complete-profile" import ModalRegisterAccount from "./ModalRegisterAccount" +import moment from "moment" const ModalRegister = ({ show, handleClose }) => { const [showModalCompleteProfile, setShowModalCompleteProfile] = useState( @@ -27,6 +28,8 @@ const ModalRegister = ({ show, handleClose }) => { }) const onSubmit = async data => { + data.birthdate = moment(data.birthdate).format("YYYY-MM-DD") + await postRegisterFull(data) .then(response => { window.alert( diff --git a/frontend/src/components/complete-profile.js b/frontend/src/components/complete-profile.js index de18ddd93db03e083a96475d20f2f88a6f4a7c86..89e956aad77e60def0d885efc965712a3069afe2 100644 --- a/frontend/src/components/complete-profile.js +++ b/frontend/src/components/complete-profile.js @@ -5,6 +5,13 @@ import { Controller } from "react-hook-form" import PhoneInput from "react-phone-input-2" import "react-phone-input-2/lib/style.css" import "./user-form.css" +import { ThemeProvider } from "@material-ui/core/styles" +import theme from "../styles/theme" +import MomentUtils from "@date-io/moment" +import { + KeyboardDatePicker, + MuiPickersUtilsProvider, +} from "@material-ui/pickers" import { useAuth } from "../hooks/authenticate" function CompleteProfile({ @@ -21,526 +28,542 @@ function CompleteProfile({ }) { const { closeModalChangeProfile } = useAuth() return ( - - + + + + - -

- {modalName} -

+ +

+ {modalName} +

-
-

- Informasi Pribadi -

+
+

+ Informasi Pribadi +

- -
- -
- No. KTP/SIM/Passport - * -
-
-
-
- - {errors.id_card_no && ( - - {errors.id_card_no.message} - - )} -
-
- - -
- -
- Nama Lengkap - * -
-
-
-
- - {errors.first_name && ( - - {errors.first_name.message} - - )} -
-
- - -
- -
- Tempat Lahir - * -
-
-
-
- - {errors.birthplace && ( - - {errors.birthplace.message} - - )} -
-
- - -
- -
- Tanggal Lahir - * -
-
-
-
- - {errors.birthdate && ( - - {errors.birthdate.message} - - )} -
-
- - -
- -
- Berat Badan - * -
-
-
-
- - {errors.body_weight && ( - - {errors.body_weight.message} - - )} -
-
- - - -
- Jenis Kelamin - * -
-
- -
-
- + +
+ +
+ No. KTP/SIM/Passport + * +
+
-
- +
+ + {errors.id_card_no && ( + + {errors.id_card_no.message} + + )}
-
- {errors.sex && ( - - {errors.sex.message} - - )} - -
+ - -
- -
- Pekerjaan - * -
-
-
-
- - - - - - - - - -
-
+
+ +
+ Nama Lengkap + * +
+
+
+
+ + {errors.first_name && ( + + {errors.first_name.message} + + )} +
+ - -
- -
- Gol. Darah - * -
-
-
-
- - - - - - -
-
+
+ +
+ Tempat Lahir + * +
+
+
+
+ + {errors.birthplace && ( + + {errors.birthplace.message} + + )} +
+ - -
- -
- Status Perkawinan - * -
-
-
-
- - - - - - -
-
- -

- Alamat -

+
+ +
+ Tanggal Lahir + * +
+
+
+
+ + } + name="birthdate" + control={control} + /> + {errors.birthdate && ( + + {errors.birthdate.message} + + )} +
+ - - -
- Alamat Rumah - * -
-
- - - + +
+ +
+ Berat Badan + * +
+
+
+
+ {errors.body_weight && ( + + {errors.body_weight.message} + + )} +
+
+ + + +
+ Jenis Kelamin + * +
+
+ +
+
+ +
+
+ +
+
+ {errors.sex && ( + + {errors.sex.message} + + )}
- {errors.address && ( - - {errors.address.message} - - )} - - + + +
+ +
+ Pekerjaan + * +
+
+
+
- - + > + + + + + + + + +
+
+ + +
+ +
+ Gol. Darah + * +
+
+
+
- - + > + + + + + +
+
+ + +
+ +
+ Status Perkawinan + * +
+
+
+
- + > + + + + + +
- {errors.district && ( - - {errors.district.message} - - )} - {errors.village && ( - - {errors.village.message} - - )} - {errors.city && ( - - {errors.city.message} - - )} - -
- -
- -
Alamat Kantor
-
-
-
- -
-
+

+ Alamat +

-

- Kontak -

+ + +
+ Alamat Rumah + * +
+
+ + + + + + + {errors.address && ( + + {errors.address.message} + + )} + + + + + + + + + + + + {errors.district && ( + + {errors.district.message} + + )} + {errors.village && ( + + {errors.village.message} + + )} + {errors.city && ( + + {errors.city.message} + + )} + +
- -
- -
- No. Telepon/HP - * -
-
-
-
- +
+ +
Alamat Kantor
+
+
+
+ - } - name="phone_no" - control={control} - /> - {errors.phone_no && ( - - {errors.phone_no.message} - - )} -
- +
+
- -
- -
Email Kantor
-
-
-
- - {errors.work_email && ( - - {errors.work_email.message} - - )} -
-
+

+ Kontak +

- -
- -
No. Telepon Kantor
-
-
-
- +
+ +
+ No. Telepon/HP + * +
+
+
+
+ + } + name="phone_no" + control={control} /> - } - name="work_phone_no" - control={control} - /> -
- + {errors.phone_no && ( + + {errors.phone_no.message} + + )} +
+
- {isLoggedIn && ( -
- - -
- )} +
+ +
Email Kantor
+
+
+
+ + {errors.work_email && ( + + {errors.work_email.message} + + )} +
+
- {!isLoggedIn && ( -
- +
+ +
No. Telepon Kantor
+
+
+
+ + } + name="work_phone_no" + control={control} + /> +
+ + + {isLoggedIn && ( +
+ + +
+ )} + + {!isLoggedIn && ( +
+ +
+ )}
- )} -
- - + + + + ) } export default CompleteProfile diff --git a/frontend/src/components/dropzone.css b/frontend/src/components/dropzone.css new file mode 100644 index 0000000000000000000000000000000000000000..f8f75ce9aa2b52f049523f834ca99673cb4455f1 --- /dev/null +++ b/frontend/src/components/dropzone.css @@ -0,0 +1,96 @@ +@import "../styles/global.css"; + +@keyframes shake { + 0% { + transform: translate(2px, 1px) rotate(0deg); + } + 10% { + transform: translate(-1px, -2px) rotate(-1deg); + } + 20% { + transform: translate(-3px, 0px) rotate(1deg); + } + 30% { + transform: translate(0px, 2px) rotate(0deg); + } + 40% { + transform: translate(1px, -1px) rotate(1deg); + } + 50% { + transform: translate(-1px, 2px) rotate(-1deg); + } + 60% { + transform: translate(-3px, 1px) rotate(0deg); + } + 70% { + transform: translate(2px, 1px) rotate(-1deg); + } + 80% { + transform: translate(-1px, -1px) rotate(1deg); + } + 90% { + transform: translate(2px, 2px) rotate(0deg); + } + 100% { + transform: translate(1px, -2px) rotate(-1deg); + } +} + +.dzu-dropzone { + overflow: hidden; + border: 2px solid var(--red); + border-radius: 0.3rem; + border-style: dashed; +} +.dzu-dropzone:hover { + border: 2px solid var(--dark-red); + box-shadow: 0 0 2px 1px var(--dark-red); +} + +.dzu-inputLabel { + color: var(--blue); + font-family: "Poppins", sans-serif; + text-align: center; + font-size: 16px; + font-weight: 500; +} +.dzu-inputLabel:hover { + text-shadow: 0.7px 0 0 currentColor; +} + +.dzu-previewImage { + max-height: 200px; + max-width: 200px; +} + +.dzu-previewContainer { + pointer-events: none; + padding: 20px; + justify-content: center; + border-bottom: 0; + flex-wrap: wrap; + text-align: center; +} + +.dzu-previewContainer span { + color: red; + font-size: 14px; +} + +.dzu-previewStatusContainer { + pointer-events: auto; +} + +.dzu-previewButton { + pointer-events: auto; + background-size: auto; +} + +.dzu-previewContainer:hover { + animation: shake 1s linear infinite; +} + +.dzu-previewFileNameError { + color: var(--blue) !important; + margin-right: 10px; +} diff --git a/frontend/src/components/form-validation-schema.js b/frontend/src/components/form-validation-schema.js index d0bfc7c428734cb181b97799b0cf863b78ab413c..d40e8667de3d1a2dde9f52f8419e9967b46e83a1 100644 --- a/frontend/src/components/form-validation-schema.js +++ b/frontend/src/components/form-validation-schema.js @@ -4,20 +4,20 @@ import moment from "moment" const AccountSchema = yup.object().shape({ email: yup .string() - .email("Masukkan email yang valid.") - .required("Masukkan email."), + .required("Masukkan email.") + .email("Masukkan email yang valid."), password: yup .string() + .required("Masukkan password.") .min(8, "Password terlalu pendek - setidaknya harus 8 karakter.") .matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/, // NOSONAR "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."), + .required("Masukkan konfirmasi password.") + .oneOf([yup.ref("password"), null], "Password harus sama."), }) export const ProfileSchema = yup.object().shape({ @@ -41,14 +41,12 @@ export const ProfileSchema = yup.object().shape({ .matches(/^\d{16}$/, "Masukkan nomor kartu pengenal yang valid."), birthplace: yup.string().required("Masukkan tempat kelahiran."), birthdate: yup - .string() + .date() .required("Masukkan tanggal lahir.") - .test("is-the-right-format", "Masukkan tanggal lahir.", function(value) { - return moment(new Date(value), "YYYY-MM-DD", true).isValid() - }) + .typeError("Masukkan tanggal yang valid.") .test( "is-the-legal-age", - "Mohon maaf, usia Anda tidak memenuhi persyaratan.", + "Mohon maaf, usia Anda tidak memenuhi persyaratan. Usia harus dalam rentang 17-60 tahun.", function(value) { let age = moment().diff(moment(new Date(value), "YYYY-MM-DD"), "years") return age >= 17 && age <= 60 @@ -83,6 +81,9 @@ export const ChangePasswordSchema = yup.object().shape({ .oneOf([yup.ref("new_password"), null], "Password harus sama."), }) +const SUPPORTED_FORMATS = ["image/jpg", "image/jpeg", "image/gif", "image/png"] +export const MAX_IMAGE_SIZE = 52428800 + export const EventSubmissionSchema = yup.object().shape({ nama_institusi: yup.string().required("Masukkan nama institusi."), alamat_institusi: yup.string().required("Masukkan alamat institusi."), @@ -104,19 +105,43 @@ export const EventSubmissionSchema = yup.object().shape({ .email("Masukkan email yang valid."), no_telp_koor: yup.number().required("Masukkan nomor telepon koordinator."), alamat_lokasi_donor: yup.string().required("Masukkan alamat lokasi donor."), - waktu_donor: yup - .string() - .required("Masukkan tanggal dan jam pelaksanaan donor.") - .test("datetime-is-in-the-right-format", "Masukkan waktu donor.", function( - value - ) { - return moment(new Date(value), "YYYY-MM-DD HH:mm", true).isValid() - }) + tanggal_donor: yup + .date() + .required("Masukkan tanggal pelaksanaan donor.") + .typeError("Masukkan tanggal yang valid.") + .min(moment().subtract(1, "days"), "Tanggal tidak boleh di waktu lampau."), + jam_mulai: yup + .date() + .required("Masukkan jam mulai pelaksanaan donor.") + .typeError("Masukkan waktu yang valid.") .test( - "time-exceeded-today", - "Tanggal acara donor harus melebihi hari ini.", + "is-3-hours-from-now", + "Jam mulai minimal harus lebih 3 jam dari sekarang.", function(value) { - return moment(new Date(value)).isAfter(moment(), "day") + const { tanggal_donor } = this.parent + const date = moment(tanggal_donor, "YYYY-MM-DD") + const time = moment(value, "HH:mm") + const dateTime = date.set({ + hour: time.get("hour"), + minute: time.get("minute"), + second: 0, + millisecond: 0, + }) + return dateTime.isSameOrAfter(moment().add(3, "hours")) + } + ), + jam_selesai: yup + .date() + .required("Masukkan jam berakhir pelaksanaan donor.") + .typeError("Masukkan waktu yang valid.") + .test( + "is-1-hour-after-start-time", + "Jam berakhir minimal harus lebih 1 jam dari jam mulai.", + function(value) { + const { jam_mulai } = this.parent + return moment(value, "HH:mm").isSameOrAfter( + moment(jam_mulai, "HH:mm").add(1, "hours") + ) } ), perkiraan_jumlah_donor: yup @@ -126,4 +151,16 @@ export const EventSubmissionSchema = yup.object().shape({ .positive("Masukkan perkiraan jumlah donor yang valid.") .moreThan(10, "Perkiraan jumlah pendonor harus lebih dari 10 orang."), keterangan: yup.string(), + foto_lokasi: yup + .mixed() + .test( + "is-in-the-right-size", + "Ukuran gambar terlalu besar. Ukuran gambar tidak boleh melebihi 50 MB.", + value => !value || value.size <= MAX_IMAGE_SIZE + ) + .test( + "is-in-the-right-format", + "Gambar harus dalam format jpg, jpeg, gif, atau png.", + value => !value || SUPPORTED_FORMATS.includes(value.type) + ), }) diff --git a/frontend/src/components/navbar.test.js b/frontend/src/components/navbar.test.js index 0ceee6f08c588b024e1dccd90230260570505d94..b89d2f76a838ef1945bf5f244add41672859aacb 100644 --- a/frontend/src/components/navbar.test.js +++ b/frontend/src/components/navbar.test.js @@ -24,7 +24,6 @@ import { } from "../hooks/authenticate.helper" import { fillEventDonorSubmission } from "./ModalEventDonorSubmissionHelper" import { render } from "../utils/test-util" - import Navbar from "./navbar" describe(`Navbar`, () => { @@ -339,9 +338,7 @@ describe(`Register`, () => { 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 password./)).toBeInTheDocument() expect( await findByText(/Masukkan konfirmasi password./) ).toBeInTheDocument() @@ -499,7 +496,7 @@ describe(`Ajukan Acara Donor`, () => { fireEvent.click(screen.getByText("Ajukan Acara Donor")) expect(screen.getByLabelText(/Alamat Institusi/i)).toBeInTheDocument() - expect(screen.getByLabelText(/Alamat Lokasi Donor/i)).toBeInTheDocument() + expect(screen.getByText(/Informasi Acara Donor/i)).toBeInTheDocument() fireEvent.click(screen.getByText("×")) await waitForElementToBeRemoved(() => @@ -524,14 +521,17 @@ describe(`Ajukan Acara Donor`, () => { alamat_lokasi_donor: "Sekre Pacil", email_kantor: "pacil@cs.ui.ac.id", email_koor: "pacilia@gmail.com", + foto_lokasi: undefined, + kategori: "Terbuka", + kecamatan: "Beji", keterangan: "", nama_institusi: "Pacilkom", nama_koor: "dr. Pacilia", - no_telp_kantor: "08165342342", - no_telp_koor: "08167021743", - nomor: "ea582779-fdfc-444e-ad03-4084e715af0a", - perkiraan_jumlah_donor: 455, - waktu_donor: "2020-05-05T12:13:00", + no_telp_kantor: "+62 123 456 789 0", + no_telp_koor: "+62 789 456 123", + perkiraan_jumlah_donor: "89", + waktu_berakhir: "2020-06-25T15:26", + waktu_mulai: "2020-06-25T12:01", }, }) @@ -566,34 +566,33 @@ describe(`Ajukan Acara Donor`, () => { expect(await findByText(/Masukkan nama institusi./)).toBeInTheDocument() expect(await findByText(/Masukkan alamat institusi./)).toBeInTheDocument() expect(await findByText(/Masukkan email kantor./)).toBeInTheDocument() + expect( + await findByText(/Masukkan nomor telepon kantor./) + ).toBeInTheDocument() + expect(await findByText(/Masukkan nama koordinator./)).toBeInTheDocument() - expect(await findByText(/Masukkan email kantor./)).toBeInTheDocument() expect(await findByText(/Masukkan email koordinator./)).toBeInTheDocument() expect( await findByText(/Masukkan nomor telepon koordinator./) ).toBeInTheDocument() + expect( await findByText(/Masukkan alamat lokasi donor./) ).toBeInTheDocument() expect( - await findByText(/Masukkan tanggal dan jam pelaksanaan donor./) + await findByText(/Masukkan tanggal pelaksanaan donor./) + ).toBeInTheDocument() + expect( + await findByText(/Masukkan jam mulai pelaksanaan donor./) + ).toBeInTheDocument() + expect( + await findByText(/Masukkan jam berakhir pelaksanaan donor./) ).toBeInTheDocument() expect( await findByText(/Masukkan perkiraan jumlah donor./) ).toBeInTheDocument() }) - it(`can cancel registration when user close complete profile modal`, async () => { - render() - fireEvent.click(screen.getByText("Masuk")) - await doSuccessfulLogin() - - expect(await screen.findByText(/Ajukan Acara Donor/i)).toBeInTheDocument() - fireEvent.click(screen.getByText("Ajukan Acara Donor")) - - fireEvent.click(screen.getByText("×")) - }) - it(`shows error alert to user if event donor submission fails due to backend error`, async () => { const error = new Error("Bad request") error.response = { diff --git a/frontend/src/components/user-form.css b/frontend/src/components/user-form.css index c7170fe30fce6db8becf4bc0f87876aca72a13f5..a914d391148ac2585185a06b503659250bd225b4 100644 --- a/frontend/src/components/user-form.css +++ b/frontend/src/components/user-form.css @@ -18,17 +18,49 @@ h5 { } .react-tel-input .flag-dropdown { + outline: var(--dark-red); + background-color: var(--red); + border: 2px var(--dark-red); + border-radius: 5px 0 0 5px; +} + +.react-tel-input .flag-dropdown.open .selected-flag { outline: var(--red); background-color: var(--dark-red); border: 2px var(--red); border-radius: 5px 0 0 5px; } -.react-tel-input .selected-flag .arrow { +.react-tel-input .flag-dropdown .selected-flag:hover { + outline: var(--red); + background-color: var(--dark-red); + border: 2px var(--red); + border-radius: 5px 0 0 5px; +} +.react-tel-input .flag-dropdown .selected-flag:focus { + outline: var(--red); + background-color: var(--dark-red); + border: 2px var(--red); + border-radius: 5px 0 0 5px; +} + +.react-tel-input .selected-flag .arrow.up { border-top: none; border-bottom: 4px solid var(--white); } +.react-tel-input .selected-flag .arrow { + position: relative; + top: 50%; + margin-top: -2px; + left: 20px; + width: 0; + height: 0; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 4px solid var(--white); +} + .radio { margin: 16px 0; display: block; diff --git a/frontend/src/hooks/authenticate.helper.js b/frontend/src/hooks/authenticate.helper.js index 5b749726bd592ba5c656aa92f017e065d5a47066..23904fc335289d16f38070d66b879722ae034afc 100644 --- a/frontend/src/hooks/authenticate.helper.js +++ b/frontend/src/hooks/authenticate.helper.js @@ -4,6 +4,7 @@ import { waitForElementToBeRemoved, } from "@testing-library/react" import React from "react" +import moment from "moment" import { GoogleLogin } from "react-google-login" import { useToggle } from "react-use" import { getUserProfile, postUserLogin } from "../api" @@ -133,7 +134,10 @@ export const fillCompleteProfileForm = async () => { }) fireEvent.change(screen.getByLabelText(/Tanggal lahir/i), { target: { - value: "1999-08-10", + value: moment() + .endOf("day") + .subtract(20, "years") + .format("DD/MM/YYYY"), }, }) fireEvent.change(screen.getByLabelText(/Berat badan/i), { diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 4ca1cfd7323e2a285eb43046e5c726bd2d14915d..bc4a19e06d7e197baf912061b11482c917679871 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -24,12 +24,11 @@ body { color: var(--blue); background-color: var(--white); border-color: var(--red) !important; - box-shadow: 0 0 2px 1px var(--red) !important; } .form-control:hover { color: var(--blue); - text-shadow: 0 0 0.65px var(--blue), 0 0 0.65px var(--blue); + text-shadow: 0.7px 0 0 currentColor; background-color: var(--white); border-color: var(--dark-red) !important; box-shadow: 0 0 2px 1px var(--dark-red) !important; @@ -37,12 +36,16 @@ body { .form-control:focus { color: var(--blue); - text-shadow: 0 0 0.65px var(--blue), 0 0 0.65px var(--blue); + text-shadow: 0.7px 0 0 currentColor; background-color: var(--white); border-color: var(--dark-red) !important; box-shadow: 4px 4px var(--dark-red) !important; } +input { + color: var(--blue) !important; +} + .text-red { color: var(--red); } diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js new file mode 100644 index 0000000000000000000000000000000000000000..5e7454b974d636ec50e38bac12a7e6c8ffe75d8d --- /dev/null +++ b/frontend/src/styles/theme.js @@ -0,0 +1,54 @@ +import { createMuiTheme } from "@material-ui/core/styles" + +const RED = "#a91320" +const DARK_RED = "#8a1324" +const BLUE = "#376c8b" +const DARK_BLUE = "#285e9b" +const CREAM = "#fff3df" +const DARK_CREAM = "#f9e5cc" +const ERROR_RED = "#ff0000" + +const theme = createMuiTheme({ + palette: { + primary: { + main: RED, + dark: DARK_RED, + contrastText: CREAM, + }, + secondary: { + main: BLUE, + dark: DARK_BLUE, + contrastText: CREAM, + }, + error: { + main: ERROR_RED, + }, + }, + overrides: { + MuiIconButton: { + root: { + color: BLUE, + }, + }, + MuiPickersBasePicker: { + pickerView: { + backgroundColor: CREAM, + }, + pickerViewLandscape: { + backgroundColor: CREAM, + }, + }, + MuiPickersCalendarHeader: { + iconButton: { + backgroundColor: DARK_CREAM, + }, + }, + MuiPickersClock: { + clock: { + backgroundColor: DARK_CREAM, + }, + }, + }, +}) + +export default theme