diff --git a/informasi_fasilitas/admin.py b/informasi_fasilitas/admin.py index 16f4a849df7954cd199653404a88808252e56da2..7e2232ab7d8bd3d888b31c2f376d839a45880544 100644 --- a/informasi_fasilitas/admin.py +++ b/informasi_fasilitas/admin.py @@ -1,10 +1,36 @@ -from django.contrib import admin -from .models import Lokasi, Fasilitas, Komentar, Kegiatan, KomentarKegiatan, FotoKegiatan +from django.contrib import admin, messages +from django.utils.translation import gettext_lazy as _ + +from notification.utils import send_komentar_notification, get_target_fcm_device +from .models import Lokasi, Fasilitas, KomentarFasilitas, Kegiatan, KomentarKegiatan, FotoKegiatan + + +class KomentarAdmin(admin.ModelAdmin): + list_display = ('__str__', 'user', 'created', 'notify_to') + actions = ('send_notification',) + + def send_notification(self, request, queryset): + for komentar in queryset: + result = send_komentar_notification(komentar) + if result: + if result['success']: + to = get_target_fcm_device(komentar).first() + msg = _('sent to %s, detail: %r' % (to, result)) + self.message_user(request, msg) + else: + msg = _('failed to send, detail: %r' % (result)) + self.message_user(request, msg, level=messages.WARNING) + + send_notification.short_description = _('Send notification') + + def notify_to(self, obj): + return get_target_fcm_device(obj).first() + notify_to.admin_order_field = 'user' # Register your models here. admin.site.register(Lokasi) admin.site.register(Fasilitas) -admin.site.register(Komentar) +admin.site.register(KomentarFasilitas, KomentarAdmin) admin.site.register(Kegiatan) -admin.site.register(KomentarKegiatan) +admin.site.register(KomentarKegiatan, KomentarAdmin) admin.site.register(FotoKegiatan) diff --git a/informasi_fasilitas/migrations/0017_auto_20210520_0336.py b/informasi_fasilitas/migrations/0017_auto_20210520_0336.py new file mode 100644 index 0000000000000000000000000000000000000000..c0494ed2de95ad2ce309ca3b3252fed5e058a8dd --- /dev/null +++ b/informasi_fasilitas/migrations/0017_auto_20210520_0336.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.7 on 2021-05-20 03:36 + +from django.db import migrations +import pplbackend.custom_model_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0016_kegiatan_links'), + ] + + operations = [ + migrations.AlterField( + model_name='fotokegiatan', + name='foto', + field=pplbackend.custom_model_field.CompressedImageField(default=None, null=True, upload_to='kegiatan/'), + ), + ] diff --git a/informasi_fasilitas/migrations/0017_auto_20210531_1003.py b/informasi_fasilitas/migrations/0017_auto_20210531_1003.py new file mode 100644 index 0000000000000000000000000000000000000000..7a899d07c7c7cbdcc7097ad8bd82029a14130a57 --- /dev/null +++ b/informasi_fasilitas/migrations/0017_auto_20210531_1003.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.7 on 2021-05-31 10:03 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('informasi_fasilitas', '0016_kegiatan_links'), + ] + + operations = [ + migrations.RenameModel( + old_name='Komentar', + new_name='KomentarFasilitas', + ), + migrations.RenameField( + model_name='komentarfasilitas', + old_name='date_time', + new_name='created', + ), + ] diff --git a/informasi_fasilitas/migrations/0018_auto_20210520_0451.py b/informasi_fasilitas/migrations/0018_auto_20210520_0451.py new file mode 100644 index 0000000000000000000000000000000000000000..a00898b87a87d3fff173de1ba762f71c829f4d2b --- /dev/null +++ b/informasi_fasilitas/migrations/0018_auto_20210520_0451.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.7 on 2021-05-20 04:51 + +from django.db import migrations +import pplbackend.custom_model_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0017_auto_20210520_0336'), + ] + + operations = [ + migrations.AlterField( + model_name='fotokegiatan', + name='foto', + field=pplbackend.custom_model_field.CompressedImageField(default=None, null=True, quality=85, upload_to='kegiatan/'), + ), + ] diff --git a/informasi_fasilitas/migrations/0018_auto_20210531_1202.py b/informasi_fasilitas/migrations/0018_auto_20210531_1202.py new file mode 100644 index 0000000000000000000000000000000000000000..83474a83be289cface066bdaf91d71aa906e8997 --- /dev/null +++ b/informasi_fasilitas/migrations/0018_auto_20210531_1202.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.7 on 2021-05-31 12:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0017_auto_20210531_1003'), + ] + + operations = [ + migrations.RenameField( + model_name='komentarfasilitas', + old_name='fasilitas', + new_name='informasi', + ), + migrations.RenameField( + model_name='komentarkegiatan', + old_name='kegiatan', + new_name='informasi', + ), + ] diff --git a/informasi_fasilitas/migrations/0019_auto_20210527_2251.py b/informasi_fasilitas/migrations/0019_auto_20210527_2251.py new file mode 100644 index 0000000000000000000000000000000000000000..b325187c60e11a59cc41d729625aec1898ba3d18 --- /dev/null +++ b/informasi_fasilitas/migrations/0019_auto_20210527_2251.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.7 on 2021-05-27 22:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0018_auto_20210520_0451'), + ] + + operations = [ + migrations.RemoveField( + model_name='kegiatan', + name='narahubung', + ), + migrations.AddField( + model_name='kegiatan', + name='nama_kontak', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AddField( + model_name='kegiatan', + name='nomor_kontak', + field=models.CharField(default='111111111111111', max_length=15), + ), + migrations.AddField( + model_name='kegiatan', + name='zona_waktu', + field=models.CharField(choices=[('WIB', 'Disabilitas Fisik'), ('WITA', 'Disabilitas Mental'), ('WIT', 'Disabilitas Intelektual')], default='WIB', max_length=4), + ), + ] diff --git a/informasi_fasilitas/migrations/0020_auto_20210601_0503.py b/informasi_fasilitas/migrations/0020_auto_20210601_0503.py new file mode 100644 index 0000000000000000000000000000000000000000..d35236b7a9c41f46a25b56698f79f714d574dde6 --- /dev/null +++ b/informasi_fasilitas/migrations/0020_auto_20210601_0503.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1.7 on 2021-06-01 05:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0019_auto_20210527_2251'), + ] + + operations = [ + migrations.AlterField( + model_name='kegiatan', + name='zona_waktu', + field=models.CharField(choices=[('WIB', 'WIB'), ('WITA', 'WITA'), ('WIT', 'WIT')], default='WIB', max_length=4), + ), + migrations.AlterUniqueTogether( + name='fasilitas', + unique_together={('lokasi', 'tag')}, + ), + ] diff --git a/informasi_fasilitas/migrations/0020_merge_20210531_1241.py b/informasi_fasilitas/migrations/0020_merge_20210531_1241.py new file mode 100644 index 0000000000000000000000000000000000000000..16c7bb87890f21624bebc9be7a90836ded68ce97 --- /dev/null +++ b/informasi_fasilitas/migrations/0020_merge_20210531_1241.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-31 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0018_auto_20210531_1202'), + ('informasi_fasilitas', '0019_auto_20210527_2251'), + ] + + operations = [ + ] diff --git a/informasi_fasilitas/migrations/0021_auto_20210531_1529.py b/informasi_fasilitas/migrations/0021_auto_20210531_1529.py new file mode 100644 index 0000000000000000000000000000000000000000..3b13d22b4163e5bd268fd3b8c2fc4b91b62ad6fa --- /dev/null +++ b/informasi_fasilitas/migrations/0021_auto_20210531_1529.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-31 15:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0020_merge_20210531_1241'), + ] + + operations = [ + migrations.AlterField( + model_name='kegiatan', + name='zona_waktu', + field=models.CharField(choices=[('WIB', 'WIB'), ('WITA', 'WITA'), ('WIT', 'WIT')], default='WIB', max_length=4), + ), + ] diff --git a/informasi_fasilitas/migrations/0021_auto_20210601_0631.py b/informasi_fasilitas/migrations/0021_auto_20210601_0631.py new file mode 100644 index 0000000000000000000000000000000000000000..37927c1e7537e41defabbddcbdecb1b6ae1c1371 --- /dev/null +++ b/informasi_fasilitas/migrations/0021_auto_20210601_0631.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.7 on 2021-06-01 06:31 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('informasi_fasilitas', '0020_auto_20210601_0503'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='dislikes', + unique_together={('user', 'fasilitas')}, + ), + migrations.AlterUniqueTogether( + name='likes', + unique_together={('user', 'fasilitas')}, + ), + ] diff --git a/informasi_fasilitas/migrations/0022_merge_20210602_1726.py b/informasi_fasilitas/migrations/0022_merge_20210602_1726.py new file mode 100644 index 0000000000000000000000000000000000000000..ecbdc4d010a4ec8dab29f99c0c9239718c814f7e --- /dev/null +++ b/informasi_fasilitas/migrations/0022_merge_20210602_1726.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-06-02 17:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0021_auto_20210531_1529'), + ('informasi_fasilitas', '0021_auto_20210601_0631'), + ] + + operations = [ + ] diff --git a/informasi_fasilitas/models.py b/informasi_fasilitas/models.py index 7745493a26ce75058beed0fe5342e1af3a0a9f70..bad7bb408ccc2f49b01bb77ef0f27433a2861be5 100644 --- a/informasi_fasilitas/models.py +++ b/informasi_fasilitas/models.py @@ -1,7 +1,7 @@ from django.db import models from django.contrib.auth.models import User from multiselectfield import MultiSelectField - +from pplbackend.custom_model_field import CompressedImageField from django.utils import timezone import string @@ -45,6 +45,12 @@ JENIS_DISABILITAS = ( ('DS', 'Disabilitas Sensorik'), ) +TIMEZONE_INDONESIA = ( + ('WIB', 'WIB'), + ('WITA', 'WITA'), + ('WIT', 'WIT'), +) + def _default_lokasi_place_id(): return \ ''.join([random.choice(string.ascii_letters) for _ in range(40)]) @@ -73,6 +79,9 @@ class Fasilitas(models.Model): image = models.ImageField(upload_to="fasilitas/", null=True, default=None) is_verified = models.BooleanField(default=False) + class Meta: + unique_together = ('lokasi', 'tag') + class Kegiatan(models.Model): objects = models.Manager() @@ -80,19 +89,29 @@ class Kegiatan(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) nama_kegiatan = models.CharField(max_length=50) penyelenggara = models.CharField(max_length=50) - narahubung = models.TextField(null=True) + nama_kontak = models.CharField(max_length=50, null=True, blank=True) + nomor_kontak = models.CharField(max_length=15, unique=False, null=False, default="1"*15) deskripsi = models.TextField(null=False) links = models.TextField(null=True, blank=True) + zona_waktu = models.CharField(choices=TIMEZONE_INDONESIA, null=False, max_length=4, default="WIB") time_start = models.DateTimeField(blank=False, null=False, default=timezone.now) time_end = models.DateTimeField(blank=True, null=True) class Komentar(models.Model): objects = models.Manager() - fasilitas = models.ForeignKey(Fasilitas, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) - date_time = models.DateTimeField(auto_now_add=True) deskripsi = models.TextField() + created = models.DateTimeField(auto_now_add=True) + + class Meta: + abstract = True + + def __str__(self): + return self.deskripsi + +class KomentarFasilitas(Komentar): + informasi = models.ForeignKey(Fasilitas, on_delete=models.CASCADE) class Likes(models.Model): objects = models.Manager() @@ -100,20 +119,23 @@ class Likes(models.Model): fasilitas = models.ForeignKey(Fasilitas, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) + class Meta: + unique_together = ('user', 'fasilitas') + class Dislikes(models.Model): objects = models.Manager() user = models.ForeignKey(User, on_delete=models.CASCADE) fasilitas = models.ForeignKey(Fasilitas, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) -class KomentarKegiatan(models.Model): - objects = models.Manager() - user = models.ForeignKey(User, on_delete=models.CASCADE) - kegiatan = models.ForeignKey(Kegiatan, on_delete=models.CASCADE) - deskripsi = models.TextField() - created = models.DateTimeField(auto_now_add=True) + class Meta: + unique_together = ('user', 'fasilitas') + +class KomentarKegiatan(Komentar): + informasi = models.ForeignKey(Kegiatan, on_delete=models.CASCADE) + class FotoKegiatan(models.Model): objects = models.Manager() kegiatan = models.ForeignKey(Kegiatan, related_name="images", on_delete=models.CASCADE) - foto = models.ImageField(upload_to="kegiatan/", null=True, default=None) + foto = CompressedImageField(upload_to="kegiatan/", null=True, default=None, quality=85) diff --git a/informasi_fasilitas/serializers.py b/informasi_fasilitas/serializers.py index 8b034aef7b9308862534aa442bb618a76eba4979..8bcc4389f5825268af9ffc8d593deccd6d6dfde4 100644 --- a/informasi_fasilitas/serializers.py +++ b/informasi_fasilitas/serializers.py @@ -1,6 +1,22 @@ from rest_framework import serializers -from .models import Lokasi, Kegiatan, KomentarKegiatan, FotoKegiatan +from .models import Lokasi, Fasilitas, Kegiatan, \ + KomentarKegiatan, FotoKegiatan, Likes, Dislikes, JENIS_DISABILITAS +import pytz + +TIMEZONE_INDONESIA = { + "WIB": pytz.timezone("Asia/Jakarta"), + "WITA": pytz.timezone("Asia/Makassar"), + "WIT": pytz.timezone("Asia/Jayapura"), +} + + +def time_converter(time, zona_waktu): + if zona_waktu in TIMEZONE_INDONESIA: + time = time.astimezone(TIMEZONE_INDONESIA[zona_waktu]) + else: + time = time.astimezone("Asia/Jakarta") + return time class LokasiSerializer(serializers.ModelSerializer): @@ -18,11 +34,86 @@ class LokasiSerializer(serializers.ModelSerializer): } -class FotoKegiatanSerializer(serializers.ModelSerializer): +class FasilitasSerializer(serializers.ModelSerializer): + class Meta: + model = Fasilitas + fields = ("id", "date_time", "deskripsi", "like", + "dislike", "rating", "tag", "disabilitas", + "jumlah", "image", "is_verified") + + +class FasilitasSerializerWrite(FasilitasSerializer): + disabilitas = serializers.MultipleChoiceField(required=True, + choices=JENIS_DISABILITAS) + + class Meta: + model = FasilitasSerializer.Meta.model + fields = FasilitasSerializer.Meta.fields + ('lokasi', 'user') + extra_kwargs = { + 'lokasi': {'required': True}, + 'user': {'required': True}, + 'tag': {"required": True}, + 'deskripsi':{"required": True}, + 'rating': {"required": True}, + 'disabilitas': {"required": True}, + 'jumlah': {"required": True}, + } + + def to_internal_value(self, data): + parsed_data = data.copy() + input_disabilitas = parsed_data.get('disabilitas') + if input_disabilitas is not None: + parsed_data['disabilitas'] = input_disabilitas.split() + return super().to_internal_value(parsed_data) + + +class FasilitasSerializerRead(FasilitasSerializer): place_id = serializers.CharField(source='lokasi.place_id', read_only=True) + creator = serializers.CharField(source='user.last_name', read_only=True) + creator_email = serializers.CharField(source='user.email', read_only=True) + date_time = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() + like = serializers.SerializerMethodField() + dislike = serializers.SerializerMethodField() + + def get_date_time(self, obj): + time = obj.date_time + return time.strftime("%d-%m-%Y %H:%M:%S") + + def get_image(self, obj): + image = obj.image + return '/media/' + str(image) + + def get_like(self, obj): + likes = obj.likes_set.count() + return likes + + def get_dislike(self, obj): + dislike = obj.dislikes_set.count() + return dislike + class Meta: - model = FotoKegiatan + model = FasilitasSerializer.Meta.model + fields = FasilitasSerializer.Meta.fields + ('place_id', 'creator', "creator_email") + + +class LikesSerializer(serializers.ModelSerializer): + class Meta: + model = Likes fields = '__all__' + + +class DislikesSerializer(serializers.ModelSerializer): + class Meta: + model = Dislikes + fields = '__all__' + +class FotoKegiatanSerializer(serializers.ModelSerializer): + place_id = serializers.CharField(source='kegiatan.lokasi.place_id', read_only=True) + + class Meta: + model = FotoKegiatan + fields = ("id", "kegiatan", "place_id", "foto") extra_kwargs = { 'foto': {'required': True}, 'kegiatan': {'required': True}, @@ -32,27 +123,53 @@ class FotoKegiatanSerializer(serializers.ModelSerializer): class KegiatanSerializer(serializers.ModelSerializer): place_id = serializers.CharField(source='lokasi.place_id', read_only=True) creator = serializers.CharField(source='user.last_name', read_only=True) + creator_email = serializers.CharField(source='user.email', read_only=True) class Meta: model = Kegiatan - fields = ('id', 'place_id', 'creator', 'lokasi', 'user', + fields = ('id', 'place_id', 'creator', 'lokasi', 'user', 'creator_email', 'nama_kegiatan', 'penyelenggara', 'deskripsi', - "links","narahubung", 'time_start', 'time_end') + "links", "nomor_kontak", "nama_kontak", + "zona_waktu", 'time_start', 'time_end') + extra_kwargs = { 'nama_kegiatan': {'required': True}, 'penyelenggara': {'required': True}, 'deskripsi': {'required': True}, - 'narahubung': {"required": True}, + 'nomor_kontak': {"required": True}, + 'zona_waktu': {"required": True}, 'time_start': {'required': True}, } +class KegiatanSerializerRead(KegiatanSerializer): + time_start = serializers.SerializerMethodField() + time_end = serializers.SerializerMethodField() + + def get_time_start(self, obj): + time = obj.time_start + zona_waktu = obj.zona_waktu + time = time_converter(time, zona_waktu) + return time.strftime('%Y-%m-%d %H:%M') + + def get_time_end(self, obj): + time = obj.time_end + zona_waktu = obj.zona_waktu + if time is None: + return None + time = time_converter(time, zona_waktu) + return time.strftime('%Y-%m-%d %H:%M') + class KomentarKegiatanSerializer(serializers.ModelSerializer): - kegiatan = serializers.IntegerField(source='kegiatan.id', read_only=True) + kegiatan = serializers.IntegerField(source='informasi.id', read_only=True) creator = serializers.CharField(source='user.last_name', read_only=True) + creator_email = serializers.CharField(source='user.email', read_only=True) + creator_picture = serializers.ImageField(source='user.bisagouser.foto', read_only=True) + class Meta: model = KomentarKegiatan - fields = ('id', 'creator', 'kegiatan', 'deskripsi', 'created') + fields = ('id', 'creator', 'kegiatan', 'deskripsi', 'created', + 'creator_email', 'creator_picture') extra_kwargs = { 'deskripsi': {'required': True}, 'created': {'required' : True} diff --git a/informasi_fasilitas/test_admin.py b/informasi_fasilitas/test_admin.py new file mode 100644 index 0000000000000000000000000000000000000000..ca177eaf4951c29e1fbc91f8ff138553564a12a4 --- /dev/null +++ b/informasi_fasilitas/test_admin.py @@ -0,0 +1,86 @@ +from unittest.mock import MagicMock, patch +from django_mock_queries.query import MockSet + +from django.contrib import messages +from django.contrib.admin.sites import AdminSite +from django.test import TestCase +from django.utils.translation import gettext_lazy as _ + +from .models import Komentar +from .admin import KomentarAdmin + +class MockSuperUser: + def has_perm(self, perm, obj=None): + return True + +class MockRequest: + def __init__(self): + self.GET = [] + self.user = MockSuperUser() + +request = MockRequest() + +class TestAdmin(TestCase): + result_success = {'success':1} + result_failed = {'success':0} + + def setUp(self): + self.site = AdminSite() + self.komentar_admin = KomentarAdmin(Komentar, self.site) + + self.mock_fcm_device = MagicMock() + self.mock_fcm_device.__str__.return_value = 'ariq' + self.mock_fcm_device_query = MockSet(self.mock_fcm_device) + + self.mock_komentar = MagicMock() + self.mock_komentar_query = MockSet(self.mock_komentar) + + def test_list_display(self): + list_display = self.komentar_admin.get_list_display(request) + self.assertEqual(list_display, ('__str__', 'user', 'created', + 'notify_to')) + + def test_list_actions(self): + list_action = self.komentar_admin.get_action_choices(request) + self.assertIn(('send_notification', 'Send notification'), list_action) + + @patch('informasi_fasilitas.admin.KomentarAdmin.message_user') + @patch('informasi_fasilitas.admin.get_target_fcm_device') + @patch('informasi_fasilitas.admin.send_komentar_notification') + def test_action_send_notification_success(self, mock_send_komentar_notif, + mock_target_fcm, mock_message_user): + mock_send_komentar_notif.return_value = self.result_success + mock_target_fcm.return_value = self.mock_fcm_device_query + + self.komentar_admin.send_notification(request, self.mock_komentar_query) + + msg = _('sent to ariq, detail: %r' % self.result_success) + + mock_target_fcm.assert_called_once_with(self.mock_komentar) + mock_message_user.assert_called_once_with(request, msg) + + @patch('informasi_fasilitas.admin.KomentarAdmin.message_user') + @patch('informasi_fasilitas.admin.get_target_fcm_device') + @patch('informasi_fasilitas.admin.send_komentar_notification') + def test_action_send_notification_failed(self, mock_send_komentar_notif, + mock_target_fcm, mock_message_user): + mock_send_komentar_notif.return_value = self.result_failed + mock_target_fcm.return_value = self.mock_fcm_device_query + + self.komentar_admin.send_notification(request, self.mock_komentar_query) + + msg = _('failed to send, detail: %r' % self.result_failed) + + mock_target_fcm.assert_not_called() + mock_message_user.assert_called_once_with(request, msg, + level=messages.WARNING) + + + @patch('informasi_fasilitas.admin.get_target_fcm_device') + def test_list_display_notify_to(self, mock_target_fcm): + mock_target_fcm.return_value = self.mock_fcm_device_query + + ret = self.komentar_admin.notify_to(self.mock_komentar) + + self.assertEqual(ret, self.mock_fcm_device) + mock_target_fcm.assert_called_once_with(self.mock_komentar) diff --git a/informasi_fasilitas/test_base.py b/informasi_fasilitas/test_base.py index e481d3d379185b75e487e56f5651a6f595f38dbf..fe5a22024679ae493b19a9e6531d5d9588fc65b4 100644 --- a/informasi_fasilitas/test_base.py +++ b/informasi_fasilitas/test_base.py @@ -1,17 +1,18 @@ import json import tempfile - +import pytz from datetime import datetime, timedelta from django.test import TestCase, Client from django.contrib.auth.models import User from django.urls import reverse, path, include +from django.utils import timezone from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.base import ContentFile from .models import ( Lokasi, Fasilitas, - Komentar, + KomentarFasilitas, KURSI_RODA, Likes, Dislikes, @@ -21,6 +22,11 @@ from .models import ( ) from pplbackend.utils import get_client_login_with_user +TIMEZONE_INDONESIA = { + "WIB": pytz.timezone("Asia/Jakarta"), + "WITA": pytz.timezone("Asia/Makassar"), + "WIT": pytz.timezone("Asia/Jayapura"), +} class InformasiFasilitasTest(TestCase): put_content_type = "application/x-www-form-urlencoded" @@ -48,15 +54,89 @@ class InformasiFasilitasTest(TestCase): 'deskripsi': 'sangat membantu', } - # Waktu mungkin perlu disesuaikan lagi test dan implementasinya + time_start = timezone.now() + timedelta(days=1) + time_end = timezone.now() + timedelta(days=2) + + time_start_zone = { + "WIB": time_start.astimezone(TIMEZONE_INDONESIA["WIB"]).strftime("%Y-%m-%d %H:%M%z"), + "WITA": time_start.astimezone(TIMEZONE_INDONESIA["WITA"]).strftime("%Y-%m-%d %H:%M%z"), + "WIT": time_start.astimezone(TIMEZONE_INDONESIA["WIT"]).strftime("%Y-%m-%d %H:%M%z"), + } + + time_end_zone = { + "WIB": time_end.astimezone(TIMEZONE_INDONESIA["WIB"]).strftime("%Y-%m-%d %H:%M%z"), + "WITA": time_end.astimezone(TIMEZONE_INDONESIA["WITA"]).strftime("%Y-%m-%d %H:%M%z"), + "WIT": time_end.astimezone(TIMEZONE_INDONESIA["WIT"]).strftime("%Y-%m-%d %H:%M%z"), + } + + # mock_kegiatan_test = { + # 'nama_kegiatan': 'mock kegiatan', + # 'penyelenggara': 'mock penyelenggara', + # 'time_start':(datetime.now()+timedelta(days=1)).strftime("%Y-%m-%d %H:%M"), + # 'time_end': (datetime.now()+timedelta(days=2)).strftime("%Y-%m-%d %H:%M"), + # 'narahubung': 'sebuah narahubung', + # 'deskripsi': 'sebuah deskripsi', + # 'links': "www.example.com;www.example.com" + # } + mock_kegiatan_test = { 'nama_kegiatan': 'mock kegiatan', 'penyelenggara': 'mock penyelenggara', - 'time_start':(datetime.now()+timedelta(days=1)).strftime("%Y-%m-%d %H:%M"), - 'time_end': (datetime.now()+timedelta(days=2)).strftime("%Y-%m-%d %H:%M"), - 'narahubung': 'sebuah narahubung', + "zona_waktu": "WIB", + 'time_start': time_start_zone["WIB"], + 'time_end': time_end_zone["WIB"], + 'nama_kontak': 'sebuah narahubung', + 'nomor_kontak': "1" * 15, 'deskripsi': 'sebuah deskripsi', - 'links': "www.example.com;www.example.com" + 'links': "www.example.com" + } + + mock_kegiatan_wita_test = { + 'nama_kegiatan': 'mock kegiatan', + 'penyelenggara': 'mock penyelenggara', + "zona_waktu": "WITA", + 'time_start': time_start_zone["WITA"], + 'time_end': time_end_zone["WITA"], + 'nama_kontak': 'sebuah narahubung', + 'nomor_kontak': "1" * 15, + 'deskripsi': 'sebuah deskripsi', + 'links': "www.example.com" + } + + mock_kegiatan_wit_test = { + 'nama_kegiatan': 'mock kegiatan', + 'penyelenggara': 'mock penyelenggara', + "zona_waktu": "WIT", + 'time_start': time_start_zone["WIT"], + 'time_end': time_end_zone["WIT"], + 'nama_kontak': 'sebuah narahubung', + 'nomor_kontak': "1" * 15, + 'deskripsi': 'sebuah deskripsi', + 'links': "www.example.com" + } + + mock_kegiatan_time_order_test = { + 'nama_kegiatan': 'mock kegiatan', + 'penyelenggara': 'mock penyelenggara', + "zona_waktu": "WIB", + 'time_start': time_start_zone["WIB"], + 'time_end': time_end_zone["WIB"], + 'nama_kontak': 'sebuah narahubung', + 'nomor_kontak': "1" * 15, + 'deskripsi': 'sebuah deskripsi', + 'links': "www.example.com" + } + + mock_kegiatan_name_order_test = { + 'nama_kegiatan': 'aaaa mock kegiatan', + 'penyelenggara': 'mock penyelenggara', + "zona_waktu": "WIB", + 'time_start': time_start_zone["WIB"], + 'time_end': time_end_zone["WIB"], + 'nama_kontak': 'sebuah narahubung', + 'nomor_kontak': "1" * 15, + 'deskripsi': 'sebuah deskripsi', + 'links': "www.example.com" } mock_komentar_kegiatan_test = { @@ -177,10 +257,10 @@ class InformasiFasilitasTest(TestCase): fasilitas_dict=fasilitas_dict, ) - return Komentar.objects.create( + return KomentarFasilitas.objects.create( **komentar_dict, user=user, - fasilitas=fasilitas, + informasi=fasilitas, ) def create_likes_test( @@ -284,19 +364,19 @@ class InformasiFasilitasTest(TestCase): user=None, komentar_kegiatan_dict=mock_komentar_kegiatan_test, user_dict=mock_user_test, - kegiatan=None + informasi=None ): user = self.get_or_create_user_test( user=user, user_dict=user_dict, ) - if kegiatan is None: - kegiatan = self.create_kegiatan_test() + if informasi is None: + informasi = self.create_kegiatan_test() return KomentarKegiatan.objects.create( **komentar_kegiatan_dict, - kegiatan=kegiatan, + informasi=informasi, user=user ) diff --git a/informasi_fasilitas/test_models.py b/informasi_fasilitas/test_models.py index fae0d4c450595dec53f384d2ea6dc685835074f6..9e929a50e2777784a57688f229225001d08b31fa 100644 --- a/informasi_fasilitas/test_models.py +++ b/informasi_fasilitas/test_models.py @@ -1,7 +1,8 @@ from django.db.utils import IntegrityError from .test_base import InformasiFasilitasTest -from .models import Lokasi, Fasilitas, Komentar, Likes, Dislikes, Kegiatan, FotoKegiatan, KomentarKegiatan +from .models import (Lokasi, Fasilitas, KomentarFasilitas, Likes, Dislikes, + Kegiatan, FotoKegiatan, KomentarKegiatan) class InformasiFasilitasModelTest(InformasiFasilitasTest): @@ -48,15 +49,19 @@ class InformasiFasilitasModelTest(InformasiFasilitasTest): def test_models_komentar_not_created(self): with self.assertRaises(IntegrityError) as ex: - obj = Komentar(fasilitas=None) + obj = KomentarFasilitas(informasi=None) obj.save() self.assertEqual(ex.expected, IntegrityError) def test_models_create_new_komentar(self): self.create_komentar_test() - count = Komentar.objects.all().count() + count = KomentarFasilitas.objects.all().count() self.assertNotEqual(count, 0) + def test_models_komentar_str(self): + komentar = self.create_komentar_test() + self.assertEqual(str(komentar), komentar.deskripsi) + def test_models_dislikes_not_created(self): with self.assertRaises(IntegrityError) as ex: obj = Dislikes(fasilitas=None) @@ -78,18 +83,22 @@ class InformasiFasilitasModelTest(InformasiFasilitasTest): self.create_likes_test() count = Likes.objects.all().count() self.assertNotEqual(count, 0) - + def test_models_create_new_kegiatan(self): self.create_kegiatan_test() count = Kegiatan.objects.all().count() self.assertNotEqual(count, 0) - + def test_models_create_new_foto_kegiatan(self): self.create_foto_kegiatan_test() count = FotoKegiatan.objects.all().count() self.assertNotEqual(count, 0) - + def test_models_create_new_komentar_kegiatan(self): self.create_komentar_kegiatan_test() count = KomentarKegiatan.objects.all().count() self.assertNotEqual(count, 0) + + def test_models_komentar_kegiatan_str(self): + komentar = self.create_komentar_kegiatan_test() + self.assertEqual(str(komentar), komentar.deskripsi) diff --git a/informasi_fasilitas/test_update_place_id.py b/informasi_fasilitas/test_update_place_id.py index b8330b06b2e3c99dd02be15551306fef73072254..609e34e53b06d1e69c4a9ae765ef429ff66576ac 100644 --- a/informasi_fasilitas/test_update_place_id.py +++ b/informasi_fasilitas/test_update_place_id.py @@ -21,8 +21,11 @@ class UpdatePlaceIdTest(TestCase): "place_id": "IEUBJWLNDSOHIUJL", "timestamp": time - timedelta(days=120) }) + fasilitas_dict = factory.mock_fasilitas_test.copy() + fasilitas_dict.update({"tag": 'RT'}) fasilitas1 = factory.create_fasilitas_test(user=user, lokasi=lokasi2) - fasilitas2 = factory.create_fasilitas_test(user=user, lokasi=lokasi2) + fasilitas2 = factory.create_fasilitas_test(user=user, lokasi=lokasi2, + fasilitas_dict=fasilitas_dict) @patch('informasi_fasilitas.management.commands.update_place_id.requests.get') def test_lokasi_expired(self, mock_get): diff --git a/informasi_fasilitas/test_views_fasilitas.py b/informasi_fasilitas/test_views_fasilitas.py index cbd8f6f389d11adf877c74b741d367e21e0086a9..0a589ca688634de47f7214118b723b98656f92e5 100644 --- a/informasi_fasilitas/test_views_fasilitas.py +++ b/informasi_fasilitas/test_views_fasilitas.py @@ -170,6 +170,5 @@ class FasilitasRelatedViewTest(InformasiFasilitasViewTest): 'rating': 3, 'tag': 'KR', } - response = self.client.put(self.update_fasilitas_url, data=send_data, - content_type=self.put_content_type) + response = self.client.put(self.update_fasilitas_url, data=send_data) self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) diff --git a/informasi_fasilitas/test_views_kegiatan.py b/informasi_fasilitas/test_views_kegiatan.py index cd84115ec9011aad8e555d066d0f2eaadc0ccc8c..ffe50fdc05757c8f83aec113408c0e1ca30ecf31 100644 --- a/informasi_fasilitas/test_views_kegiatan.py +++ b/informasi_fasilitas/test_views_kegiatan.py @@ -1,25 +1,29 @@ +from unittest.mock import patch +from datetime import timedelta +from http import HTTPStatus + import json import shutil import os -from datetime import timedelta -from http import HTTPStatus + from django.test import Client from django.urls import reverse from django.test import override_settings from django.utils import timezone from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings -from unittest.mock import patch + from .test_base import InformasiFasilitasViewTest -from .models import Lokasi, Kegiatan, FotoKegiatan -from pplbackend.utils import response_decode +from .models import Kegiatan, FotoKegiatan -class KegiatanRelatedViewTest(InformasiFasilitasViewTest): +class KegiatanRelatedViewTest(InformasiFasilitasViewTest): def setUp(self): super().setUp() self.media_root = os.path.join(settings.BASE_DIR, 'test_file/media') self.kegiatan = self.create_kegiatan_test(self.user, self.lokasi) + self.kegiatan_time_start = self.kegiatan.time_start[:16] + self.kegiatan_time_end = self.kegiatan.time_end[:16] self.kwargs_place_id = {'place_id': self.lokasi.place_id} self.kwargs_add_or_update_kegiatan = { 'place_id': self.lokasi.place_id, @@ -30,7 +34,15 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): 'kegiatan_id': self.kegiatan.id, } - self.kwargs_kegiatan_id = {'kegiatan_id' : self.kegiatan.lokasi.place_id} + self.kwargs_search_kegiatan = {'query': 'mock', } + self.kwargs_search_kegiatan_fail = {'query': 'this shouldnt exist', } + + self.kwargs_kegiatan_id = {'kegiatan_id': self.kegiatan.lokasi.place_id} + self.kwargs_list_kegiatan_in_order = self.kwargs_search_kegiatan + self.kwargs_list_kegiatan_in_order_fail = self.kwargs_search_kegiatan_fail + self.kwargs_list_kegiatan_in_order_with_query = { + 'query': 'aaaa mock kegiatan', + } image_path1, image_path2 = ("test_file/test1.jpg", "test_file/test2.jpg") image1 = SimpleUploadedFile("test1.jpg", content=open(image_path1, 'rb').read(), @@ -38,24 +50,60 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): image2 = SimpleUploadedFile("test2.jpg", content=open(image_path2, 'rb').read(), content_type='image/jpeg') - self.kegiatan_images = {'images': [image1, image2]} - for image in self.kegiatan_images["images"]: - FotoKegiatan.objects.create(kegiatan=self.kegiatan, foto=image) + self.kegiatan_images = {'images[]': [image1, image2]} + self.foto = None + for image in self.kegiatan_images["images[]"]: + foto = FotoKegiatan.objects.create(kegiatan=self.kegiatan, foto=image) + self.foto = foto + image.seek(0) + + self.kwargs_update_foto_kegiatan = { + 'place_id': self.lokasi.place_id, + 'kegiatan_id': self.kegiatan.id, + 'id': self.foto.id, + } self.get_list_kegiatan_url = \ reverse('list-kegiatan', kwargs=self.kwargs_place_id) + self.get_list_all_kegiatan_url = \ + reverse('list-kegiatan-all') self.get_detail_kegiatan_url = \ reverse('detail-kegiatan', kwargs=self.kwargs_add_or_update_kegiatan) self.get_nearest_kegiatan_url = \ reverse('nearest-kegiatan') + self.add_kegiatan_url = \ reverse('add-kegiatan', kwargs=self.kwargs_place_id) self.update_kegiatan_url = reverse('update-kegiatan', - kwargs=self.kwargs_add_or_update_kegiatan) + kwargs=self.kwargs_add_or_update_kegiatan) + + self.search_kegiatan_url = reverse('search-kegiatan', + kwargs=self.kwargs_search_kegiatan) + self.search_kegiatan_fail_url = reverse('search-kegiatan', + kwargs=self.kwargs_search_kegiatan_fail) self.list_foto_kegiatan_url = \ reverse('list-foto-kegiatan', kwargs=self.kwargs_get_foto_kegiatan) + self.add_foto_kegiatan_url = \ + reverse('add-foto-kegiatan', kwargs=self.kwargs_get_foto_kegiatan) + self.update_foto_kegiatan_url = \ + reverse('update-foto-kegiatan', kwargs=self.kwargs_update_foto_kegiatan) + + self.list_kegiatan_by_latest_added_url = \ + reverse('list-kegiatan-by-latest-added', kwargs=self.kwargs_list_kegiatan_in_order) + self.list_kegiatan_by_name_url = \ + reverse('list-kegiatan-by-name', kwargs=self.kwargs_list_kegiatan_in_order) + self.list_kegiatan_by_time_url = \ + reverse('list-kegiatan-by-time', kwargs=self.kwargs_list_kegiatan_in_order) + self.list_kegiatan_by_latest_added_fail_url = \ + reverse('list-kegiatan-by-latest-added', kwargs=self.kwargs_list_kegiatan_in_order_fail) + self.list_kegiatan_by_name_fail_url = \ + reverse('list-kegiatan-by-name', kwargs=self.kwargs_list_kegiatan_in_order_fail) + self.list_kegiatan_by_time_fail_url = \ + reverse('list-kegiatan-by-time', kwargs=self.kwargs_list_kegiatan_in_order_fail) + self.list_kegiatan_by_name_with_query_url = \ + reverse('list-kegiatan-by-name', kwargs=self.kwargs_list_kegiatan_in_order_with_query) def tearDown(self): try: @@ -63,7 +111,6 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): except OSError: pass - @override_settings(MEDIA_ROOT=("test_file" + '/media')) def test_can_add_kegiatan(self): Kegiatan.objects.all().delete() @@ -75,8 +122,10 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): data.pop("id", None) expected_json = self.mock_kegiatan_test.copy() expected_json.update({'creator': 'mock last_name', - 'place_id': 'mock_place_id' - }) + 'place_id': 'mock_place_id', + 'creator_email': self.mock_user_test['email'], + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end}) self.assertEqual(response.status_code, HTTPStatus.CREATED) self.assertDictEqual(data, expected_json) count = Kegiatan.objects.all().count() @@ -110,13 +159,16 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): 'id': self.kegiatan.id, 'place_id': self.kegiatan.lokasi.place_id, 'creator': self.kegiatan.user.last_name, - 'nama_kegiatan' : self.kegiatan.nama_kegiatan, + 'creator_email': self.kegiatan.user.email, + 'nama_kegiatan': self.kegiatan.nama_kegiatan, 'penyelenggara': self.kegiatan.penyelenggara, 'deskripsi': self.kegiatan.deskripsi, 'links': self.kegiatan.links, - 'narahubung': self.kegiatan.narahubung, - 'time_start': self.kegiatan.time_start, - 'time_end': self.kegiatan.time_end, + 'nama_kontak': self.kegiatan.nama_kontak, + 'nomor_kontak': self.kegiatan.nomor_kontak, + 'zona_waktu': self.kegiatan.zona_waktu, + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end, }, } self.assertEqual(content, expected_json) @@ -126,34 +178,98 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): self.assertEqual(response.status_code, HTTPStatus.OK) content = json.loads(response.content.decode('utf-8')) expected_json = { - 'id': self.kegiatan.id, - 'place_id': self.kegiatan.lokasi.place_id, - 'creator': self.kegiatan.user.last_name, - 'nama_kegiatan' : self.kegiatan.nama_kegiatan, - 'penyelenggara': self.kegiatan.penyelenggara, - 'deskripsi': self.kegiatan.deskripsi, - 'links': self.kegiatan.links, - 'narahubung': self.kegiatan.narahubung, - 'time_start': self.kegiatan.time_start, - 'time_end': self.kegiatan.time_end, + 'id': self.kegiatan.id, + 'place_id': self.kegiatan.lokasi.place_id, + 'creator': self.kegiatan.user.last_name, + 'creator_email': self.kegiatan.user.email, + 'nama_kegiatan': self.kegiatan.nama_kegiatan, + 'penyelenggara': self.kegiatan.penyelenggara, + 'deskripsi': self.kegiatan.deskripsi, + 'links': self.kegiatan.links, + 'nama_kontak': self.kegiatan.nama_kontak, + 'nomor_kontak': self.kegiatan.nomor_kontak, + 'zona_waktu': self.kegiatan.zona_waktu, + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end, } self.assertEqual(content, expected_json) + def test_can_get_detail_kegiatan_wita(self): + Kegiatan.objects.all().delete() + kegiatan_wita = self.create_kegiatan_test(kegiatan_dict=self.mock_kegiatan_wita_test, + user=self.user, lokasi=self.lokasi) + get_detail_kegiatan_url = \ + reverse('detail-kegiatan', kwargs={ + 'place_id': kegiatan_wita.lokasi.place_id, + 'id': kegiatan_wita.id, + }) + response = Client().get(get_detail_kegiatan_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + expected_json = { + 'id': kegiatan_wita.id, + 'place_id': kegiatan_wita.lokasi.place_id, + 'creator': kegiatan_wita.user.last_name, + 'creator_email': kegiatan_wita.user.email, + 'nama_kegiatan': kegiatan_wita.nama_kegiatan, + 'penyelenggara': kegiatan_wita.penyelenggara, + 'deskripsi': kegiatan_wita.deskripsi, + 'links': kegiatan_wita.links, + 'nama_kontak': kegiatan_wita.nama_kontak, + 'nomor_kontak': kegiatan_wita.nomor_kontak, + 'zona_waktu': kegiatan_wita.zona_waktu, + 'time_start': kegiatan_wita.time_start[:16], + 'time_end': kegiatan_wita.time_end[:16], + } + self.assertDictEqual(content, expected_json) + + def test_can_get_detail_kegiatan_wit(self): + Kegiatan.objects.all().delete() + kegiatan_wit = self.create_kegiatan_test(kegiatan_dict=self.mock_kegiatan_wit_test, + user=self.user, lokasi=self.lokasi) + get_detail_kegiatan_url = \ + reverse('detail-kegiatan', kwargs={ + 'place_id': kegiatan_wit.lokasi.place_id, + 'id': kegiatan_wit.id, + }) + response = Client().get(get_detail_kegiatan_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + expected_json = { + 'id': kegiatan_wit.id, + 'place_id': kegiatan_wit.lokasi.place_id, + 'creator': kegiatan_wit.user.last_name, + 'creator_email': kegiatan_wit.user.email, + 'nama_kegiatan': kegiatan_wit.nama_kegiatan, + 'penyelenggara': kegiatan_wit.penyelenggara, + 'deskripsi': kegiatan_wit.deskripsi, + 'links': kegiatan_wit.links, + 'nama_kontak': kegiatan_wit.nama_kontak, + 'nomor_kontak': kegiatan_wit.nomor_kontak, + 'zona_waktu': kegiatan_wit.zona_waktu, + 'time_start': kegiatan_wit.time_start[:16], + 'time_end': kegiatan_wit.time_end[:16], + } + self.assertDictEqual(content, expected_json) + def test_can_get_nearest_kegiatan(self): response = Client().get(self.get_nearest_kegiatan_url) self.assertEqual(response.status_code, HTTPStatus.OK) content = json.loads(response.content.decode('utf-8')) expected_json = { - 'id': self.kegiatan.id, - 'place_id': self.kegiatan.lokasi.place_id, - 'creator': self.kegiatan.user.last_name, - 'nama_kegiatan' : self.kegiatan.nama_kegiatan, - 'penyelenggara': self.kegiatan.penyelenggara, - 'deskripsi': self.kegiatan.deskripsi, - 'links': self.kegiatan.links, - 'narahubung': self.kegiatan.narahubung, - 'time_start': self.kegiatan.time_start, - 'time_end': self.kegiatan.time_end, + 'id': self.kegiatan.id, + 'place_id': self.kegiatan.lokasi.place_id, + 'creator': self.kegiatan.user.last_name, + 'creator_email': self.kegiatan.user.email, + 'nama_kegiatan': self.kegiatan.nama_kegiatan, + 'penyelenggara': self.kegiatan.penyelenggara, + 'deskripsi': self.kegiatan.deskripsi, + 'links': self.kegiatan.links, + 'nama_kontak': self.kegiatan.nama_kontak, + 'nomor_kontak': self.kegiatan.nomor_kontak, + 'zona_waktu': self.kegiatan.zona_waktu, + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end, } self.assertEqual(content, expected_json) @@ -175,9 +291,12 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): data.pop("id", None) expected_json = self.mock_kegiatan_test.copy() expected_json.update({'creator': 'mock last_name', - 'place_id': 'mock_place_id' - }) - send_data.pop("images") + 'place_id': 'mock_place_id', + 'creator_email': self.mock_user_test['email'], + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end}) + + send_data.pop("images[]") expected_json.update(send_data) self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) self.assertDictEqual(data, expected_json) @@ -187,7 +306,6 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): self.assertEqual(count, 1) self.assertEqual(count_foto, 4) - def test_update_kegiatan_lokasi_not_exist(self): Kegiatan.objects.all().delete() url = reverse('update-kegiatan', kwargs={ @@ -202,13 +320,193 @@ class KegiatanRelatedViewTest(InformasiFasilitasViewTest): response = self.client.put(url, data=send_data) self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) - def test_can_get_list_foto_kegiatan(self): response = Client().get(self.list_foto_kegiatan_url) data = response.json() self.assertEqual(len(data), 2) counter = 1 for _, item in data.items(): - self.assertIn("test"+str(counter), item["foto"]) + self.assertIn("test" + str(counter), item["foto"]) self.assertEqual(item["kegiatan"], self.kegiatan.id) counter += 1 + + def test_can_add_foto_kegiatan(self): + data = {"foto": self.kegiatan_images["images[]"][0]} + response = self.client.post(self.add_foto_kegiatan_url, data=data) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + kegiatan_fotos = FotoKegiatan.objects.all() + self.assertEqual(len(kegiatan_fotos), 3) + + def test_failed_add_foto_kegiatan(self): + data = {"foto": self.kegiatan_images["images[]"][0]} + url = reverse('add-foto-kegiatan', kwargs={ + 'place_id': "IDSNKCM", + 'kegiatan_id': 222, + }) + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + kegiatan_fotos = FotoKegiatan.objects.all() + self.assertEqual(len(kegiatan_fotos), 2) + + def test_can_put_update_foto_kegiatan(self): + data = {"foto": self.kegiatan_images["images[]"][0]} + response = self.client.put(self.update_foto_kegiatan_url, data=data) + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) + output = response.json() + self.assertIsNotNone(output.get("foto")) + self.assertNotEqual(self.foto.foto, output.get("foto")) + + def test_failed_put_update_foto_kegiatan(self): + data = {"foto": self.kegiatan_images["images[]"][0]} + url = reverse('update-foto-kegiatan', kwargs={ + 'place_id': "IDSNKCM", + 'kegiatan_id': 222, + 'id': 444, + }) + response = self.client.put(url, data=data) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_search_kegiatan(self): + response = Client().get(self.search_kegiatan_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + expected_json = { + str(self.kegiatan.id): { + 'id': self.kegiatan.id, + 'place_id': self.kegiatan.lokasi.place_id, + 'creator': self.kegiatan.user.last_name, + 'creator_email': self.kegiatan.user.email, + 'nama_kegiatan' : self.kegiatan.nama_kegiatan, + 'penyelenggara': self.kegiatan.penyelenggara, + 'deskripsi': self.kegiatan.deskripsi, + 'links': self.kegiatan.links, + 'nama_kontak': self.kegiatan.nama_kontak, + 'nomor_kontak': self.kegiatan.nomor_kontak, + 'zona_waktu': self.kegiatan.zona_waktu, + 'time_start': self.kegiatan_time_start, + 'time_end': self.kegiatan_time_end, + }, + } + self.assertEqual(content, expected_json) + + def test_search_kegiatan_empty_result_or_fail(self): + response = Client().get(self.search_kegiatan_fail_url) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_get_list_kegiatan_by_name_order(self): + Kegiatan.objects.all().delete() + + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + base_mock_id = str(content['id']) + + response_params = self.mock_kegiatan_name_order_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + ordered_name_mock_id = str(content['id']) + + response = self.client.get(self.list_kegiatan_by_name_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + + response_id_in_order = list(content.keys()) + + self.assertEqual(content[base_mock_id]['nama_kegiatan'], + self.kegiatan.nama_kegiatan) + self.assertEqual(content[ordered_name_mock_id]['nama_kegiatan'], + self.mock_kegiatan_name_order_test['nama_kegiatan']) + self.assertEqual(response_id_in_order[0], ordered_name_mock_id) + self.assertEqual(response_id_in_order[1], base_mock_id) + + def test_fail_get_list_kegiatan_by_name_order(self): + response = Client().get(self.list_kegiatan_by_name_fail_url) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_get_list_kegiatan_by_time_order(self): + Kegiatan.objects.all().delete() + + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + base_mock_id = str(content['id']) + + response_params = self.mock_kegiatan_name_order_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + ordered_time_mock_id = str(content['id']) + + response = self.client.get(self.list_kegiatan_by_time_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + + response_id_in_order = list(content.keys()) + + self.assertEqual(content[base_mock_id]['time_start'], + self.kegiatan_time_start) + self.assertEqual(content[ordered_time_mock_id]['time_start'], + self.mock_kegiatan_time_order_test['time_start'][:16]) + self.assertEqual(response_id_in_order[0], base_mock_id) + self.assertEqual(response_id_in_order[1], ordered_time_mock_id) + + def test_fail_get_list_kegiatan_by_time_order(self): + response = Client().get(self.list_kegiatan_by_time_fail_url) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_get_list_kegiatan_by_latest_added(self): + Kegiatan.objects.all().delete() + + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + + response = self.client.get(self.list_kegiatan_by_latest_added_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + + response_id_in_order = list(content.keys()) + self.assertGreater(int(response_id_in_order[0]), int(response_id_in_order[1])) + + def test_fail_get_list_kegiatan_by_latest_added(self): + response = Client().get(self.list_kegiatan_by_latest_added_fail_url) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_get_list_all_kegiatan(self): + response = Client().get(self.get_list_all_kegiatan_url) + data = response.json() + self.assertEqual(len(data), 1) + + def test_can_get_list_kegiatan_by_name_order_with_query(self): + Kegiatan.objects.all().delete() + + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + + response_params = self.mock_kegiatan_name_order_test.copy() + response = self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + content = json.loads(response.content.decode('utf-8')) + ordered_name_mock_id = str(content['id']) + + response = self.client.get(self.list_kegiatan_by_name_with_query_url) + self.assertEqual(response.status_code, HTTPStatus.OK) + content = json.loads(response.content.decode('utf-8')) + + response_id_in_order = list(content.keys()) + + self.assertEqual(len(response_id_in_order), 1) + self.assertEqual(response_id_in_order[0], ordered_name_mock_id) + self.assertEqual(content[ordered_name_mock_id]['nama_kegiatan'], + self.mock_kegiatan_name_order_test['nama_kegiatan']) diff --git a/informasi_fasilitas/test_views_komentar.py b/informasi_fasilitas/test_views_komentar.py index b81ba1f2ecd534bd28772d83ae874dbaf3694f67..458eaa00662f34c2ab11d73e543f7124483044ce 100644 --- a/informasi_fasilitas/test_views_komentar.py +++ b/informasi_fasilitas/test_views_komentar.py @@ -1,10 +1,12 @@ -import json from http import HTTPStatus from django.urls import reverse +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.files import File -from .test_base import InformasiFasilitasViewTest -from .models import Komentar from pplbackend.utils import response_decode +from registrasi.models import BisaGoUser +from .test_base import InformasiFasilitasViewTest +from .models import KomentarFasilitas from .views import TIME_FORMAT @@ -20,13 +22,16 @@ class KomentarRelatedViewTest(InformasiFasilitasViewTest): self.list_komentar_url = \ reverse('list-komentar', kwargs=self.kwargs_add_or_list_komentar) + self.image_path = "test_file/test1.jpg" + self.image = SimpleUploadedFile("test1.jpg", content=open(self.image_path, 'rb').read(), + content_type='image/jpeg') def test_can_comment_facility(self): response = \ self.client.post(self.add_komentar_url, self.mock_komentar_test) self.assertEqual(response.status_code, HTTPStatus.CREATED) - count = Komentar.objects.filter(fasilitas=self.fasilitas).count() + count = KomentarFasilitas.objects.filter(informasi=self.fasilitas).count() self.assertEqual(count, 1) def test_cannot_comment_facility_if_deskripsi_empty(self): @@ -38,17 +43,26 @@ class KomentarRelatedViewTest(InformasiFasilitasViewTest): self.assertEqual(expected_json, response_json) def test_get_list_komentar(self): + bisagouser = BisaGoUser.objects.create(user=self.user, + phone_number='0'*12, foto=self.image) + self.user.bisagouser = bisagouser + self.user.save() komentar = self.create_komentar_test(fasilitas=self.fasilitas) response = self.client.get(self.list_komentar_url) self.assertEqual(response.status_code, HTTPStatus.OK) response_json = response_decode(response) + creator_picture = response_json[str(komentar.id)].pop('creator_picture')\ + if isinstance(response_json, dict) else '' expected_json = { str(komentar.id): { 'id': komentar.id, 'deskripsi': komentar.deskripsi, 'creator': komentar.user.last_name, - 'date_time': komentar.date_time.strftime(TIME_FORMAT), + 'creator_email': komentar.user.email, + 'created': komentar.created.strftime(TIME_FORMAT), } } self.assertEqual(response_json, expected_json) + self.assertRegex(creator_picture, + r'http://testserver/media/user/test1[0-9a-zA-Z_-]*.jpg') diff --git a/informasi_fasilitas/test_views_komentar_kegiatan.py b/informasi_fasilitas/test_views_komentar_kegiatan.py index c712e2201833804497390f08361bcd7b65d3d518..abc7e61e96a0948acbe5e1789765e18e6a391908 100644 --- a/informasi_fasilitas/test_views_komentar_kegiatan.py +++ b/informasi_fasilitas/test_views_komentar_kegiatan.py @@ -29,27 +29,27 @@ class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): self.client.post(self.add_komentar_kegiatan_url, self.mock_komentar_kegiatan_test) self.assertEqual(response.status_code, HTTPStatus.CREATED) - count = KomentarKegiatan.objects.filter(kegiatan=self.kegiatan).count() + count = KomentarKegiatan.objects.filter(informasi=self.kegiatan).count() self.assertEqual(count, 1) - + def test_fail_add_komentar_kegiatan(self): KomentarKegiatan.objects.all().delete() response = \ self.client.post(self.add_komentar_kegiatan_url, None) self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) - count = KomentarKegiatan.objects.filter(kegiatan=self.kegiatan).count() + count = KomentarKegiatan.objects.filter(informasi=self.kegiatan).count() self.assertEqual(count, 0) - + def test_can_get_list_komentar_kegiatan(self): - komentar = self.create_komentar_kegiatan_test(user=self.user, kegiatan=self.kegiatan) + komentar = self.create_komentar_kegiatan_test(user=self.user, informasi=self.kegiatan) response = self.client.get(self.list_komentar_kegiatan_url) self.assertEqual(response.status_code, HTTPStatus.OK) response_json = response_decode(response) expected_id = komentar.id expected_creator = komentar.user.last_name - expected_kegiatan = komentar.kegiatan.id + expected_kegiatan = komentar.informasi.id expected_deskripsi = komentar.deskripsi self.assertEqual(response_json[str(komentar.id)]['id'], expected_id) @@ -59,7 +59,7 @@ class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): self.assertEqual(True, ('created' in response_json[str(komentar.id)].keys())) def test_can_get_komentar_kegiatan(self): - komentar = self.create_komentar_kegiatan_test(user=self.user, kegiatan=self.kegiatan) + komentar = self.create_komentar_kegiatan_test(user=self.user, informasi=self.kegiatan) kwargs_get_komentar_kegiatan = { 'place_id': self.lokasi.place_id, 'kegiatan_id': self.kegiatan.id, @@ -73,7 +73,7 @@ class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): response_json = response_decode(response) expected_id = komentar.id expected_creator = komentar.user.last_name - expected_kegiatan = komentar.kegiatan.id + expected_kegiatan = komentar.informasi.id expected_deskripsi = komentar.deskripsi self.assertEqual(response_json['id'], expected_id) @@ -81,14 +81,14 @@ class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): self.assertEqual(response_json['kegiatan'], expected_kegiatan) self.assertEqual(response_json['deskripsi'], expected_deskripsi) self.assertEqual(True, ('created' in response_json.keys())) - + def test_fail_get_komentar_kegiatan(self): response = self.client.get('/informasi-fasilitas/lokasi/get-komentar-kegiatan/harusnyaTidakAda/101/1011') self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) def test_can_delete_komentar_kegiatan(self): KomentarKegiatan.objects.all().delete() - komentar = self.create_komentar_kegiatan_test(user=self.user, kegiatan=self.kegiatan) + komentar = self.create_komentar_kegiatan_test(user=self.user, informasi=self.kegiatan) kwargs_delete_komentar_kegiatan = { 'place_id': self.lokasi.place_id, @@ -98,14 +98,13 @@ class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): delete_komentar_kegiatan_url = \ reverse('delete-komentar-kegiatan', kwargs=kwargs_delete_komentar_kegiatan) - + response = self.client.delete(delete_komentar_kegiatan_url) self.assertEqual(response.status_code, HTTPStatus.OK) - count = KomentarKegiatan.objects.filter(kegiatan=self.kegiatan).count() + count = KomentarKegiatan.objects.filter(informasi=self.kegiatan).count() self.assertEqual(count, 0) - + def test_fail_delete_komentar_kegiatan(self): response = self.client.delete('/informasi-fasilitas/lokasi/delete-komentar-kegiatan/harusnyaTidakAda/10/10101') self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) - diff --git a/informasi_fasilitas/test_views_lokasi.py b/informasi_fasilitas/test_views_lokasi.py index ab304c6e1c8db1f86f15c80301b20b381d09855a..5ce187ca5061f2ed591f83c0dc8da324a54e1c2e 100644 --- a/informasi_fasilitas/test_views_lokasi.py +++ b/informasi_fasilitas/test_views_lokasi.py @@ -13,7 +13,6 @@ from pplbackend.utils import response_decode class LokasiRelatedViewTest(InformasiFasilitasViewTest): - def setUp(self): super().setUp() self.list_lokasi_url = reverse('list-lokasi') diff --git a/informasi_fasilitas/urls.py b/informasi_fasilitas/urls.py index 9529f3ef9a6a7449b40c28efd77076ccae8a29f9..f03b11a9bb7e5c78c5f33e9c3e07327e84efcc16 100644 --- a/informasi_fasilitas/urls.py +++ b/informasi_fasilitas/urls.py @@ -32,6 +32,9 @@ urlpatterns = [ path('lokasi/list-kegiatan//', views_kegiatan.list_kegiatan, name='list-kegiatan'), + + path('lokasi/list-kegiatan-all', + views_kegiatan.list_kegiatan_all, name='list-kegiatan-all'), path('lokasi/detail-kegiatan///', views_kegiatan.detail_kegiatan, name='detail-kegiatan'), @@ -39,6 +42,9 @@ urlpatterns = [ path('lokasi/kegiatan-terdekat', views_kegiatan.nearest_kegiatan, name='nearest-kegiatan'), + path('lokasi/search-kegiatan/', + views_kegiatan.search_kegiatan, name='search-kegiatan'), + path('lokasi/add-kegiatan//', views_kegiatan.add_kegiatan, name='add-kegiatan'), @@ -48,6 +54,30 @@ urlpatterns = [ path('lokasi/list-foto-kegiatan//', views_kegiatan.list_foto_kegiatan, name='list-foto-kegiatan'), + path('lokasi/add-foto-kegiatan//', + views_kegiatan.add_foto_kegiatan, name='add-foto-kegiatan'), + + path('lokasi/update-foto-kegiatan///', + views_kegiatan.update_foto_kegiatan, name='update-foto-kegiatan'), + + path('lokasi/list-kegiatan-by-latest-added/', + views_kegiatan.list_kegiatan_by_latest_added, name='list-kegiatan-by-latest-added'), + + path('lokasi/list-kegiatan-by-latest-added', + views_kegiatan.list_kegiatan_by_latest_added, name='list-kegiatan-by-latest-added'), + + path('lokasi/list-kegiatan-by-name/', + views_kegiatan.list_kegiatan_by_name, name='list-kegiatan-by-name'), + + path('lokasi/list-kegiatan-by-name', + views_kegiatan.list_kegiatan_by_name, name='list-kegiatan-by-name'), + + path('lokasi/list-kegiatan-by-time/', + views_kegiatan.list_kegiatan_by_time, name='list-kegiatan-by-time'), + + path('lokasi/list-kegiatan-by-time', + views_kegiatan.list_kegiatan_by_time, name='list-kegiatan-by-time'), + path('lokasi/get-komentar-kegiatan///', views_komentar_kegiatan.get_komentar_kegiatan, name='get-komentar-kegiatan'), @@ -56,9 +86,7 @@ urlpatterns = [ path('lokasi/add-komentar-kegiatan//', views_komentar_kegiatan.add_komentar_kegiatan, name='add-komentar-kegiatan'), - + path('lokasi/delete-komentar-kegiatan///', views_komentar_kegiatan.delete_komentar_kegiatan, name='delete-komentar-kegiatan'), - - ] diff --git a/informasi_fasilitas/views.py b/informasi_fasilitas/views.py index bebbff7a06c1a5084eae31c9bf2ff8c3f61b0d54..17882f45f5ecfbe99b66adbc46a42fa55e647f6e 100644 --- a/informasi_fasilitas/views.py +++ b/informasi_fasilitas/views.py @@ -1,21 +1,18 @@ from http import HTTPStatus from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist -from django.db.models import F +from django.db.utils import IntegrityError from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.parsers import FileUploadParser -from rest_framework.views import APIView -from rest_framework.generics import ListCreateAPIView from rest_framework import viewsets +from rest_framework.exceptions import ValidationError +from notification.utils import send_komentar_notification -from .serializers import LokasiSerializer -from .models import Lokasi, Fasilitas, Komentar, Likes, Dislikes +from .serializers import LokasiSerializer, FasilitasSerializerRead,\ + FasilitasSerializerWrite, LikesSerializer, DislikesSerializer +from .models import Lokasi, Fasilitas, KomentarFasilitas, Likes, Dislikes from .permissions import UserPermission TIME_FORMAT = "%d-%m-%Y %H:%M:%S" @@ -62,31 +59,10 @@ def list_fasilitas(request, place_id): try: lokasi = Lokasi.objects.get(place_id=place_id) list_fasilitas = Fasilitas.objects.filter(lokasi=lokasi) - return_json = {} - for fasilitas in list_fasilitas: - user = fasilitas.user - fasilitas.like = Likes.objects.filter( - fasilitas=fasilitas).count() - fasilitas.dislike = Dislikes.objects.filter( - fasilitas=fasilitas).count() - return_json[fasilitas.id] = {} - fasilitas_details = return_json[fasilitas.id] - fasilitas_details["id"] = fasilitas.id - fasilitas_details["creator_email"] = user.email - fasilitas_details["place_id"] = fasilitas.lokasi.place_id - fasilitas_details["deskripsi"] = fasilitas.deskripsi - fasilitas_details["creator"] = fasilitas.user.last_name - fasilitas_details["date_time"] = fasilitas.date_time.strftime( - TIME_FORMAT) - fasilitas_details["like"] = fasilitas.like - fasilitas_details["dislike"] = fasilitas.dislike - fasilitas_details["rating"] = fasilitas.rating - fasilitas_details["tag"] = fasilitas.tag - fasilitas_details["disabilitas"] = fasilitas.disabilitas - fasilitas_details["jumlah"] = fasilitas.jumlah - fasilitas_details["image"] = '/media/'+str(fasilitas.image) - fasilitas_details["is_verified"] = fasilitas.is_verified - return JsonResponse(return_json, status=HTTPStatus.OK) + serializer = FasilitasSerializerRead(list_fasilitas, many=True) + data_response = serializer.data + new_dict = {item['id']: dict(item) for item in data_response} + return JsonResponse(new_dict, status=HTTPStatus.OK) except Exception as error: return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) @@ -97,34 +73,28 @@ def list_fasilitas(request, place_id): def add_fasilitas(request, place_id): try: lokasi = Lokasi.objects.get(place_id=place_id) - deskripsi = request.POST['deskripsi'] - rating = request.POST['rating'] - tag = request.POST['tag'] - disabilitas = request.POST['disabilitas'] - jumlah = request.POST['jumlah'] - image = "" - if Fasilitas.objects.filter(lokasi=lokasi.pk, tag=tag).count() > 0: - return JsonResponse({'response': 'fasilitas already exist'}, - status=HTTPStatus.OK) - if tag == "": - return JsonResponse({'response': 'tag cannot be null'}, - status=HTTPStatus.BAD_REQUEST) - if 'image' in request.FILES.keys(): - image = request.FILES['image'] - fasilitas = Fasilitas.objects.create(lokasi=lokasi, - user=request.user, - deskripsi=deskripsi, - rating=rating, - tag=tag, - disabilitas=disabilitas, - jumlah=jumlah, - image=image) + user = request.user + data = request.data.dict() + data.update({"lokasi": lokasi.id, "user": user.id}) + serializer = FasilitasSerializerWrite(data=data) + serializer.is_valid(raise_exception=True) + fasilitas = serializer.save() return JsonResponse({'response': 'fasilitas added', 'id': fasilitas.id}, status=HTTPStatus.CREATED) - except KeyError as missing_key: - return JsonResponse({'response': missing_key_message(str(missing_key))}, - status=HTTPStatus.BAD_REQUEST) - except Exception as error: + except ValidationError as error: + error_detail = error.get_full_details() + if 'non_field_errors' in error_detail: + return JsonResponse({'response': 'fasilitas already exist'}, + status=HTTPStatus.OK) + if "tag" in error_detail: + if error_detail["tag"][0]["code"] == 'invalid_choice': + return JsonResponse({'response': 'tag cannot be null'}, + status=HTTPStatus.BAD_REQUEST) + else: + return JsonResponse({'response': "Bad Request. 'tag' key is needed"}, + status=HTTPStatus.BAD_REQUEST) + raise error + except Lokasi.DoesNotExist as error: return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) @@ -134,28 +104,8 @@ def add_fasilitas(request, place_id): def detail_fasilitas(request, place_id, id): try: fasilitas = Fasilitas.objects.get(lokasi__place_id=place_id, id=id) - user = fasilitas.user - fasilitas.like = Likes.objects.filter(fasilitas=fasilitas).count() - fasilitas.dislike = Dislikes.objects.filter( - fasilitas=fasilitas).count() - return_json = {"id": fasilitas.id, - "place_id": fasilitas.lokasi.place_id, - "deskripsi": fasilitas.deskripsi, - "creator": user.last_name, - "creator_email": user.email, - "date_time": fasilitas.date_time.strftime(TIME_FORMAT), - "like": fasilitas.like, - "dislike": fasilitas.dislike, - "rating": fasilitas.rating, - "tag": fasilitas.tag, - "disabilitas": fasilitas.disabilitas, - "jumlah": fasilitas.jumlah, - "image": '/media/' + str(fasilitas.image), - "is_verified": fasilitas.is_verified} - return JsonResponse(return_json, status=HTTPStatus.OK) - except KeyError as missing_key: - return JsonResponse({'response': missing_key_message(str(missing_key))}, - status=HTTPStatus.BAD_REQUEST) + serializer = FasilitasSerializerRead(fasilitas, many=False) + return JsonResponse(serializer.data, status=HTTPStatus.OK) except Exception as error: return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) @@ -167,35 +117,15 @@ def update_fasilitas(request, place_id, id): try: fasilitas = Fasilitas.objects.get(lokasi__place_id=place_id, id=id) user_creator = fasilitas.user - desc = fasilitas.deskripsi - tag = fasilitas.tag - jumlah = fasilitas.jumlah - image = fasilitas.image - disabilitas = fasilitas.disabilitas if user_creator == request.user: - if 'deskripsi' in request.data.keys(): - desc = request.data['deskripsi'] - if 'tag' in request.data.keys(): - tag = request.data['tag'].split() - if 'jumlah' in request.data.keys(): - jumlah = request.data['jumlah'] - if 'image' in request.data.keys(): - image = request.data['image'] - if 'disabilitas' in request.data.keys(): - disabilitas = request.data['disabilitas'] - fasilitas.deskripsi = desc - fasilitas.tag = tag - fasilitas.jumlah = jumlah - fasilitas.image = image - fasilitas.disabilitas = disabilitas - fasilitas.save() + data = request.data.dict() + serializer = FasilitasSerializerWrite(fasilitas, data=data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() return JsonResponse({'response': '{} in fasilitas edited'.format(str(request.data.keys())), }, status=HTTPStatus.ACCEPTED) return JsonResponse({'response': 'Authentication failed'}, status=HTTPStatus.UNAUTHORIZED) - except KeyError as missing_key: - return JsonResponse({'response': missing_key_message(str(missing_key))}, - status=HTTPStatus.BAD_REQUEST) except Exception as error: return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) @@ -207,35 +137,32 @@ def update_like_fasilitas(request, place_id, id, operation): try: fasilitas = Fasilitas.objects.get(lokasi__place_id=place_id, id=id) user = request.user - - try: - like = Likes.objects.get(fasilitas=fasilitas, user=user) - except Likes.DoesNotExist: - like = None - try: - dislike = Dislikes.objects.get(fasilitas=fasilitas, user=user) - except Dislikes.DoesNotExist: - dislike = None + serializer_kwargs = {"user": user.id, "fasilitas": fasilitas.id} if operation == "like": - if like != None: - return JsonResponse({'response': "You have already liked this facility"}, status=HTTPStatus.ACCEPTED) - else: - Likes.objects.create(fasilitas=fasilitas, user=user) - if dislike != None: - dislike.delete() + try: + serializer = LikesSerializer(data=serializer_kwargs) + serializer.is_valid(raise_exception=True) + serializer.save() + Dislikes.objects.filter(**serializer_kwargs).delete() + except ValidationError: + return JsonResponse({'response': "You have already liked this facility"}, + status=HTTPStatus.ACCEPTED) elif operation == "dislike": - if dislike != None: - return JsonResponse({'response': "You have already disliked this facility"}, status=HTTPStatus.ACCEPTED) - else: - Dislikes.objects.create(fasilitas=fasilitas, user=user) - if like != None: - like.delete() + try: + serializer = DislikesSerializer(data=serializer_kwargs) + serializer.is_valid(raise_exception=True) + serializer.save() + Likes.objects.filter(**serializer_kwargs).delete() + except ValidationError: + return JsonResponse({'response': "You have already disliked this facility"}, + status=HTTPStatus.ACCEPTED) + fasilitas.like = Likes.objects.filter(fasilitas=fasilitas).count() fasilitas.dislike = Dislikes.objects.filter( fasilitas=fasilitas).count() - return JsonResponse({'response': "You have successfuly {}d this facility".format(operation)}, status=HTTPStatus.CREATED) - except KeyError as missing_key: - return JsonResponse({'response': missing_key_message(str(missing_key))}, status=HTTPStatus.BAD_REQUEST) + fasilitas.save() + return JsonResponse({'response': "You have successfuly {}d this facility".format(operation)}, + status=HTTPStatus.CREATED) except Exception as error: return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) @@ -248,12 +175,13 @@ def add_komentar(request, place_id, id): lokasi = Lokasi.objects.get(place_id=place_id) fasilitas = Fasilitas.objects.get(lokasi=lokasi, id=id) deskripsi = request.POST['deskripsi'] - komentar = Komentar.objects.create(fasilitas=fasilitas, + komentar = KomentarFasilitas.objects.create(informasi=fasilitas, user=request.user, deskripsi=deskripsi) + send_komentar_notification(komentar) return JsonResponse({'response': 'komentar added', 'id': komentar.id, - "created_date": komentar.date_time.strftime(TIME_FORMAT)}, status=HTTPStatus.CREATED) + "created_date": komentar.created.strftime(TIME_FORMAT)}, status=HTTPStatus.CREATED) except KeyError as e: return JsonResponse({'response': missing_key_message(str(e))}, status=HTTPStatus.BAD_REQUEST) except Exception as e: @@ -267,7 +195,7 @@ def list_komentar(request, place_id, id): try: lokasi = Lokasi.objects.get(place_id=place_id) fasilitas = Fasilitas.objects.get(lokasi=lokasi, id=id) - list_komentar = Komentar.objects.filter(fasilitas=fasilitas) + list_komentar = KomentarFasilitas.objects.filter(informasi=fasilitas) return_json = {} for komentar in list_komentar: return_json[komentar.id] = {} @@ -275,7 +203,15 @@ def list_komentar(request, place_id, id): komentar_details["id"] = komentar.id komentar_details["deskripsi"] = komentar.deskripsi komentar_details["creator"] = komentar.user.last_name - komentar_details["date_time"] = komentar.date_time.strftime( + komentar_details["creator_email"] = komentar.user.email + foto = komentar.user.bisagouser.foto + try: + url = foto.url + except ValueError: + url = None + komentar_details["creator_picture"] =\ + request.build_absolute_uri(url) if url else None + komentar_details["created"] = komentar.created.strftime( TIME_FORMAT) return JsonResponse(return_json, status=HTTPStatus.OK) except KeyError as key: diff --git a/informasi_fasilitas/views_kegiatan.py b/informasi_fasilitas/views_kegiatan.py index 7bf9c2c23586bf21abefabda55b57230bc495f28..559ebd2984fc26842068c850f7135eab0354024d 100644 --- a/informasi_fasilitas/views_kegiatan.py +++ b/informasi_fasilitas/views_kegiatan.py @@ -9,15 +9,27 @@ from rest_framework.views import APIView from rest_framework.exceptions import AuthenticationFailed, NotFound from .models import Kegiatan, Lokasi, FotoKegiatan from django.contrib.auth.models import User -from .serializers import KegiatanSerializer, FotoKegiatanSerializer +from .serializers import KegiatanSerializer, KegiatanSerializerRead, FotoKegiatanSerializer +EMPTY_KEGIATAN = 'No Kegiatan available' @api_view(['GET']) @authentication_classes([]) @permission_classes([]) def list_kegiatan(request, place_id): queryset = Kegiatan.objects.filter(lokasi__place_id=place_id) - serializer = KegiatanSerializer(queryset, many=True) + serializer = KegiatanSerializerRead(queryset, many=True) + data_response = serializer.data + new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in data_response} + return JsonResponse(new_dict, status=HTTPStatus.OK) + + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def list_kegiatan_all(request): + queryset = _filter_kegiatan() + serializer = KegiatanSerializerRead(queryset, many=True) data_response = serializer.data new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in data_response} return JsonResponse(new_dict, status=HTTPStatus.OK) @@ -29,7 +41,7 @@ def list_kegiatan(request, place_id): def detail_kegiatan(request, place_id, id): try: queryset = Kegiatan.objects.get(lokasi__place_id=place_id, id=id) - serializer = KegiatanSerializer(queryset, many=False) + serializer = KegiatanSerializerRead(queryset, many=False) return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.OK) except Kegiatan.DoesNotExist: raise NotFound(detail="Kegiatan doesn't exist") @@ -42,8 +54,8 @@ def nearest_kegiatan(request): time_now = timezone.now() queryset = Kegiatan.objects.filter(time_start__gte=time_now).order_by('time_start').first() if queryset is None: - raise NotFound(detail="No Kegiatan available") - serializer = KegiatanSerializer(queryset, many=False) + raise NotFound(detail=EMPTY_KEGIATAN) + serializer = KegiatanSerializerRead(queryset, many=False) return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.OK) @api_view(['POST']) @@ -55,12 +67,12 @@ def add_kegiatan(request, place_id): user = request.user data = request.data.dict() data.update({"user": user.id, "lokasi": lokasi.id}) - data.pop("images", []) + data.pop("images[]", []) serializer = KegiatanSerializer(data=data) serializer.is_valid(raise_exception=True) kegiatan = serializer.save() - serializer = _add_foto_kegiatan(kegiatan, request.data.getlist("images")) + serializer = _add_foto_kegiatan(kegiatan, request.data.getlist("images[]")) return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.CREATED) except Lokasi.DoesNotExist: @@ -74,11 +86,11 @@ def update_kegiatan(request, place_id, id): try: kegiatan = Kegiatan.objects.get(lokasi__place_id=place_id, id=id) data = request.data.dict() - data.pop("images", []) + data.pop("images[]", []) serializer = KegiatanSerializer(kegiatan, data=data, partial=True) serializer.is_valid(raise_exception=True) kegiatan = serializer.save() - serializer = _add_foto_kegiatan(kegiatan, request.data.getlist("images")) + serializer = _add_foto_kegiatan(kegiatan, request.data.getlist("images[]")) return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.ACCEPTED) except Kegiatan.DoesNotExist: @@ -96,16 +108,96 @@ def list_foto_kegiatan(request, place_id, kegiatan_id): return JsonResponse(new_dict, status=HTTPStatus.OK) +@api_view(['POST']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def add_foto_kegiatan(request, place_id, kegiatan_id): + try: + kegiatan = Kegiatan.objects.get(lokasi__place_id=place_id, id=kegiatan_id) + data = request.data.dict() + data.update({"kegiatan": kegiatan.id}) + + serializer = FotoKegiatanSerializer(data=data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return JsonResponse(serializer.data, status=HTTPStatus.CREATED) + except Kegiatan.DoesNotExist: + raise NotFound(detail="Kegiatan doesn't exist") + + +@api_view(['PUT']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def update_foto_kegiatan(request, place_id, kegiatan_id, id): + try: + foto_kegiatan = FotoKegiatan.objects.get(kegiatan__lokasi__place_id=place_id, + kegiatan_id=kegiatan_id, + id=id) + serializer = FotoKegiatanSerializer(foto_kegiatan, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + return JsonResponse(serializer.data, status=HTTPStatus.ACCEPTED) + except FotoKegiatan.DoesNotExist: + raise NotFound(detail="Foto Kegiatan doesn't exist") + + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def search_kegiatan(request, query): + queryset = _filter_kegiatan(query) + if not queryset.exists(): + raise NotFound(detail=EMPTY_KEGIATAN) + serializer = KegiatanSerializerRead(queryset, many=True) + new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in serializer.data} + return JsonResponse(new_dict, status=HTTPStatus.OK) + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def list_kegiatan_by_name(request, query=None): + time_now = timezone.now() + queryset = _filter_kegiatan(query).order_by('nama_kegiatan') + if not queryset.exists(): + raise NotFound(detail=EMPTY_KEGIATAN) + serializer = KegiatanSerializerRead(queryset, many=True) + new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in serializer.data} + return JsonResponse(new_dict, status=HTTPStatus.OK) + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def list_kegiatan_by_time(request, query=None): + time_now = timezone.now() + queryset = _filter_kegiatan(query).order_by('time_start') + if not queryset.exists(): + raise NotFound(detail=EMPTY_KEGIATAN) + serializer = KegiatanSerializerRead(queryset, many=True) + new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in serializer.data} + return JsonResponse(new_dict, status=HTTPStatus.OK) + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def list_kegiatan_by_latest_added(request, query=None): + time_now = timezone.now() + queryset = _filter_kegiatan(query).order_by('-id') + if not queryset.exists(): + raise NotFound(detail=EMPTY_KEGIATAN) + serializer = KegiatanSerializerRead(queryset, many=True) + new_dict = {item['id']: _clean_json_kegiatan(dict(item)) for item in serializer.data} + return JsonResponse(new_dict, status=HTTPStatus.OK) + def _clean_json_kegiatan(kegiatan): - kegiatan.pop("user") - kegiatan.pop("lokasi") - return kegiatan + kegiatan.pop("user") + kegiatan.pop("lokasi") + return kegiatan def _add_foto_kegiatan(kegiatan, list_image): _create_list_kegiatan_foto(kegiatan, list_image) kegiatan.refresh_from_db() - return KegiatanSerializer(kegiatan) + return KegiatanSerializerRead(kegiatan) def _create_list_kegiatan_foto(kegiatan, list_image): @@ -116,7 +208,11 @@ def _create_list_kegiatan_foto(kegiatan, list_image): return list_kegiatan_foto - - - - +def _filter_kegiatan(query=None): + if query is None: + return Kegiatan.objects.all() + else: + query_by_nama = Kegiatan.objects.filter(nama_kegiatan__icontains=query) + query_by_deskripsi = Kegiatan.objects.filter(deskripsi__icontains=query) + query_by_penyelenggara = Kegiatan.objects.filter(penyelenggara__icontains=query) + return query_by_nama | query_by_deskripsi | query_by_penyelenggara diff --git a/informasi_fasilitas/views_komentar_kegiatan.py b/informasi_fasilitas/views_komentar_kegiatan.py index eea287289c64f3e0ed31379665e372da73a6467e..6dd21133fae3391d901fc6b9f5efd2a34db418cb 100644 --- a/informasi_fasilitas/views_komentar_kegiatan.py +++ b/informasi_fasilitas/views_komentar_kegiatan.py @@ -1,29 +1,21 @@ from http import HTTPStatus from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist -from django.db.models import F from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.parsers import FileUploadParser -from rest_framework.views import APIView -from rest_framework.generics import ListCreateAPIView -from rest_framework import viewsets +from notification.utils import send_komentar_notification from .serializers import KomentarKegiatanSerializer -from .models import Lokasi, Kegiatan, KomentarKegiatan -from .permissions import UserPermission +from .models import Kegiatan, KomentarKegiatan @api_view(['GET']) @authentication_classes([]) @permission_classes([]) def list_komentar_kegiatan(request, place_id, kegiatan_id): try: - queryset = KomentarKegiatan.objects.filter(kegiatan__lokasi__place_id=place_id, kegiatan__id=kegiatan_id) + queryset = KomentarKegiatan.objects.filter(informasi__lokasi__place_id=place_id, informasi__id=kegiatan_id) serializer = KomentarKegiatanSerializer(queryset, many=True) data_response = serializer.data new_dict = {item['id']: dict(item) for item in data_response} @@ -36,8 +28,8 @@ def list_komentar_kegiatan(request, place_id, kegiatan_id): @permission_classes([]) def get_komentar_kegiatan(request, place_id, kegiatan_id, komentar_id): try: - komentar = KomentarKegiatan.objects.get(kegiatan__lokasi__place_id=place_id, - kegiatan__id=kegiatan_id, id=komentar_id) + komentar = KomentarKegiatan.objects.get(informasi__lokasi__place_id=place_id, + informasi__id=kegiatan_id, id=komentar_id) serializer = KomentarKegiatanSerializer(komentar, many=False) return JsonResponse(serializer.data, status=HTTPStatus.OK) except ObjectDoesNotExist: @@ -52,10 +44,11 @@ def add_komentar_kegiatan(request, place_id, kegiatan_id): deskripsi = request.POST['deskripsi'] komentar = KomentarKegiatan.objects \ .create( - kegiatan=kegiatan, + informasi=kegiatan, user=request.user, deskripsi=deskripsi ) + send_komentar_notification(komentar) return JsonResponse({'response': 'komentar kegiatan added', 'id': komentar.id}, status=HTTPStatus.CREATED) except Exception as error: @@ -66,7 +59,7 @@ def add_komentar_kegiatan(request, place_id, kegiatan_id): @permission_classes([IsAuthenticated]) def delete_komentar_kegiatan(request, place_id, kegiatan_id, komentar_id): try: - komentar = KomentarKegiatan.objects.get(kegiatan__lokasi__place_id=place_id, kegiatan__id=kegiatan_id, id=komentar_id) + komentar = KomentarKegiatan.objects.get(informasi__lokasi__place_id=place_id, informasi__id=kegiatan_id, id=komentar_id) komentar.delete() return JsonResponse({'response': 'komentar kegiatan deleted', 'id': komentar_id}, status=HTTPStatus.OK) diff --git a/layanan_khusus/tests.py b/layanan_khusus/tests.py index 19c698d4697da053b2f97aac93905a10ba464bef..278f86d94bfbee033e8aef40e06193e6a2343a50 100644 --- a/layanan_khusus/tests.py +++ b/layanan_khusus/tests.py @@ -1,13 +1,13 @@ import json from http import HTTPStatus -from django.test import TestCase, Client, override_settings -from django.conf import settings +from django.test import TestCase, Client from django.db.utils import IntegrityError from django.contrib.auth.models import User from django.urls import path, include, reverse + +from pplbackend.utils import get_client_login_with_user from .models import Sekolah, Penyandang, Komunitas from .serializers import SekolahSerializer, KomunitasSerializer -import django ID = 'id' NAME = 'name' @@ -118,25 +118,11 @@ def penyandang_setup(): def auth_setup(): email = 'mock_user@email.com' - passcode = 'pass12345' - Client().post('/api/register/', { - 'name': 'name', - 'email': email, - 'phone_number': 000000000, - 'password': passcode - }) - test_user = User.objects.get(username=email) - test_user.is_active = True - test_user.save() - token_response = Client().post('/api-token-auth/', - {'username': email, 'password': passcode}) - content = json.loads(token_response.content.decode('utf-8')) - token = content['token'] - return Client(HTTP_AUTHORIZATION='token '+token) + user = User.objects.create_user(username=email) + return get_client_login_with_user(user) class LayananKhususModelTest(TestCase): - def test_models_sekolah_not_created(self): with self.assertRaises(IntegrityError) as ex: obj = Sekolah(name=None) @@ -170,9 +156,7 @@ class LayananKhususModelTest(TestCase): count = Komunitas.objects.all().count() self.assertNotEqual(count, 0) - class LayananKhususViewsTest(TestCase): - urlpatterns = [ path('layanan-khusus/', include('layanan_khusus.urls')), ] @@ -297,7 +281,6 @@ class LayananKhususViewsTest(TestCase): class LayananKhususSearchTest(TestCase): - urlpatterns = [ path('layanan-khusus/', include('layanan_khusus.urls')), ] diff --git a/layanan_khusus/views.py b/layanan_khusus/views.py index 251c0c2466ef850fb65e5602eaf5c776099b8a1c..ad646bfa2211131d8101764a4afa1c98a64278fd 100644 --- a/layanan_khusus/views.py +++ b/layanan_khusus/views.py @@ -8,6 +8,7 @@ from rest_framework.permissions import IsAuthenticated from .models import Sekolah, Komunitas from .serializers import SekolahSerializer, PenyandangSerializer, KomunitasSerializer from django.contrib.postgres.search import SearchVector, SearchQuery +from django.forms.models import model_to_dict @api_view(['GET']) @authentication_classes([]) @@ -86,28 +87,13 @@ def pencarian(request): return_json = {} for sekolah in list_sekolah: - return_json[indeks] = {} - sekolah_details = return_json[indeks] - sekolah_details["name"] = sekolah.name - sekolah_details["alamat"] = sekolah.alamat - sekolah_details["no_telp"] = sekolah.no_telp - sekolah_details["website"] = sekolah.website - sekolah_details["jumlah_siswa"] = sekolah.jumlah_siswa - sekolah_details["status"] = sekolah.status - sekolah_details["jenis_sekolah"] = sekolah.jenis_sekolah - sekolah_details["akreditasi"] = sekolah.akreditasi - + return_json[indeks] = model_to_dict(sekolah) + return_json[indeks].pop("id") indeks += 1 for komunitas in list_komunitas: - return_json[indeks] = {} - komunitas_details = return_json[indeks] - komunitas_details["name"] = komunitas.name - komunitas_details["alamat"] = komunitas.alamat - komunitas_details["no_telp"] = komunitas.no_telp - komunitas_details["website"] = komunitas.website - komunitas_details["jenis_komunitas"] = komunitas.jenis_komunitas - + return_json[indeks] = model_to_dict(komunitas) + return_json[indeks].pop("id") indeks += 1 return JsonResponse(return_json, status=HTTPStatus.OK) diff --git a/new_rest_api/permissions.py b/new_rest_api/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..918eb91ccdd2ae665ce5484207e889fa0c6026af --- /dev/null +++ b/new_rest_api/permissions.py @@ -0,0 +1,16 @@ +from rest_framework import permissions + +class UserViewPermission(permissions.BasePermission): + def has_permission(self, request, view): + if view.action in ['register', 'retrieve', 'activate']: + return True + if view.action in ['update', 'authorize']: + return request.user.is_authenticated + return False + + def has_object_permission(self, request, view, obj): + if view.action == 'retrieve': + return True + if view.action == 'update': + return request.user.id == obj.user.id + return False diff --git a/new_rest_api/renderers.py b/new_rest_api/renderers.py new file mode 100644 index 0000000000000000000000000000000000000000..33065cec37b35230994dd00864619e7810881e82 --- /dev/null +++ b/new_rest_api/renderers.py @@ -0,0 +1,4 @@ +from rest_framework.renderers import TemplateHTMLRenderer + +class AccountActivationHTMLRenderer(TemplateHTMLRenderer): + template_name = 'acc_activate_success.html' diff --git a/new_rest_api/test_permissions.py b/new_rest_api/test_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..e726f9c8b94b4cfd81cd3d479eb39f7753f12246 --- /dev/null +++ b/new_rest_api/test_permissions.py @@ -0,0 +1,128 @@ +from django.test import TestCase +from .permissions import UserViewPermission + +class MockUser: + def __init__(self, id=1, is_authenticated=False): + self.id = id + self.is_authenticated = is_authenticated + +class MockUserAuthenticated(MockUser): + def __init__(self, *args, **kwargs): + super().__init__(is_authenticated=True, *args, **kwargs) + +class MockUserNotAuthenticated(MockUser): + def __init__(self, *args, **kwargs): + super().__init__(is_authenticated=False, *args, **kwargs) + +class MockRequest: + def __init__(self, user): + self.user = user + +class MockView: + def __init__(self, action): + self.action = action + +class MockObject: + def __init__(self, user): + self.user = user + +class TestPermission(TestCase): + def setUp(self): + self.user_view_permission = UserViewPermission() + + self.authenticated_request = MockRequest(MockUserAuthenticated()) + self.not_authenticated_request = MockRequest(MockUserNotAuthenticated()) + + self.view_list = MockView('list') + self.view_create = MockView('register') + self.view_retrieve = MockView('retrieve') + self.view_update = MockView('update') + self.view_delete = MockView('delete') + self.view_authorize = MockView('authorize') + + # has permission tests + def test_permission_authenticated_cant_list(self): + ret = self.user_view_permission\ + .has_permission(self.authenticated_request, self.view_list) + self.assertFalse(ret) + + def test_permission_not_authenticated_cant_list(self): + ret = self.user_view_permission\ + .has_permission(self.not_authenticated_request, self.view_list) + self.assertFalse(ret) + + def test_permission_authenticated_can_create(self): + ret = self.user_view_permission\ + .has_permission(self.authenticated_request, self.view_create) + self.assertTrue(ret) + + def test_permission_not_authenticated_can_create(self): + ret = self.user_view_permission\ + .has_permission(self.not_authenticated_request, self.view_create) + self.assertTrue(ret) + + def test_permission_authenticated_can_update(self): + ret = self.user_view_permission\ + .has_permission(self.authenticated_request, self.view_update) + self.assertTrue(ret) + + def test_permission_not_authenticated_cant_update(self): + ret = self.user_view_permission\ + .has_permission(self.not_authenticated_request, self.view_update) + self.assertFalse(ret) + + def test_permission_authenticated_cant_delete(self): + ret = self.user_view_permission\ + .has_permission(self.authenticated_request, self.view_delete) + self.assertFalse(ret) + + def test_permission_not_authenticated_cant_delete(self): + ret = self.user_view_permission\ + .has_permission(self.not_authenticated_request, self.view_delete) + self.assertFalse(ret) + + def test_permission_authenticated_can_authorize(self): + ret = self.user_view_permission\ + .has_permission(self.authenticated_request, self.view_authorize) + self.assertTrue(ret) + + def test_permission_authenticated_cant_authorize(self): + ret = self.user_view_permission\ + .has_permission(self.not_authenticated_request, self.view_authorize) + self.assertFalse(ret) + + # has object permission tests + def test_object_permission_can_update_same_user(self): + user = MockUser(id=self.authenticated_request.user.id) + obj = MockObject(user) + ret = self.user_view_permission\ + .has_object_permission(self.authenticated_request, self.view_update, obj) + self.assertTrue(ret) + + def test_object_permission_cant_update_difference_user(self): + user = MockUser(id=self.authenticated_request.user.id + 1) + obj = MockObject(user) + ret = self.user_view_permission\ + .has_object_permission(self.authenticated_request, self.view_update, obj) + self.assertFalse(ret) + + def test_object_permission_can_retrieve_same_user(self): + user = MockUser(id=self.authenticated_request.user.id) + obj = MockObject(user) + ret = self.user_view_permission\ + .has_object_permission(self.authenticated_request, self.view_retrieve, obj) + self.assertTrue(ret) + + def test_object_permission_can_retrieve_difference_user(self): + user = MockUser(id=self.authenticated_request.user.id + 1) + obj = MockObject(user) + ret = self.user_view_permission\ + .has_object_permission(self.authenticated_request, self.view_retrieve, obj) + self.assertTrue(ret) + + def test_object_permission_other_action_forbidden(self): + user = MockUser(id=self.authenticated_request.user.id + 1) + obj = MockObject(user) + ret = self.user_view_permission\ + .has_object_permission(self.authenticated_request, self.view_list, obj) + self.assertFalse(ret) diff --git a/new_rest_api/test_tokens.py b/new_rest_api/test_tokens.py new file mode 100644 index 0000000000000000000000000000000000000000..2e3d5cd1094f895428111e209f42fc4c77ffe921 --- /dev/null +++ b/new_rest_api/test_tokens.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from django.utils import six +from django.test import TestCase + +from .tokens import account_activation_token + +class MockUser: + pk = 1 + is_active = False + + +class TestTokenGenerator(TestCase): + def test_make_hash_value(self): + user = MockUser() + now = datetime.now() + ret = account_activation_token._make_hash_value(user, now) + expected = (six.text_type(user.pk) + six.text_type(now) + + six.text_type(user.is_active)) + self.assertEqual(ret, expected) diff --git a/new_rest_api/test_utils.py b/new_rest_api/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1e3d93a902affe8c8260bc3be8bd747e48911f62 --- /dev/null +++ b/new_rest_api/test_utils.py @@ -0,0 +1,56 @@ +from unittest.mock import MagicMock, patch + +from django.test import TestCase +from django.test.client import RequestFactory + +from .utils import send_activation_email + +class MockUser: + pk = 1 + email = 'dummy@email.com' + username = email + last_name = 'dummy the boi' + +class TestUtilsUser(TestCase): + mock_activation_endpoint = '/api/user/activate/someuid/some-token/' + mock_email_message = MagicMock() + mock_user = MockUser() + mock_request = RequestFactory().get('') + mock_subject = 'dummy subject' + mock_token = 'some-token' + mock_uid = 'someuid' + mock_bytes = b'some bytes' + + @patch('new_rest_api.utils.EmailMessage', return_value=mock_email_message) + @patch('new_rest_api.utils.reverse', return_value=mock_activation_endpoint) + @patch('new_rest_api.utils.account_activation_token.make_token', return_value=mock_token) + @patch('new_rest_api.utils.urlsafe_base64_encode', return_value=mock_uid) + @patch('new_rest_api.utils.force_bytes', return_value=mock_bytes) + def test_send_activation_email(self, mock_force_bytes, mock_urlsafe, + mock_make_token, mock_reverse, mock_init_email): + + send_activation_email(self.mock_user, self.mock_request, subject=self.mock_subject) + + absolute_uri = self.mock_request.build_absolute_uri(self.mock_activation_endpoint) + + message = f""" +Hai {self.mock_user.last_name}, +Selamat datang di aplikasi bisaGO. +Sebelum anda bisa menggunakan akun anda, silahkan melakukan aktivasi dengan meng-klik link di bawah ini. +{absolute_uri} + +Terima kasih dan selamat menggunakan bisaGO. + + +Salam, +bisaGO dev Team +""" + + mock_force_bytes.assert_called_once_with(self.mock_user.pk) + mock_urlsafe.assert_called_once_with(self.mock_bytes) + mock_make_token.assert_called_once_with(self.mock_user) + mock_reverse.assert_called_once_with('user-activate', + kwargs={'uidb64': self.mock_uid, 'token': self.mock_token}) + mock_init_email.assert_called_once_with(self.mock_subject, message, + to=[self.mock_user.email]) + self.mock_email_message.send.assert_called_once() diff --git a/new_rest_api/test_views.py b/new_rest_api/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..68615a21bbc2004911e4cc72c9912a60e354964d --- /dev/null +++ b/new_rest_api/test_views.py @@ -0,0 +1,185 @@ +from http import HTTPStatus as status +from unittest.mock import patch, MagicMock + +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from django.urls import reverse +from django.test import TestCase + +from pplbackend.utils import response_decode, get_client_login_with_user + + +class TestUserViews(TestCase): + mock_email_message = MagicMock() + mock_token = 'some-token%s' % ('n'*27) + mock_uid = 'someuid' + + def setUp(self): + self.user = User.objects.create_user(username='test@gmail.com') + + self.register_url = reverse('user-register') + self.detail_url = reverse('user-detail', + kwargs={'user__username':'test@gmail.com'}) + self.update_url = self.detail_url + self.image = SimpleUploadedFile("test1.jpg", + content=open("test_file/test1.jpg", 'rb').read(), content_type='image/jpeg') + self.register_data = { + 'name': 'Dummy the boi', + 'email': 'dummy@test.com', + 'password': 'password', + 'phone_number': '000011112222', + 'tanggal_lahir': '2000-01-01', + 'jenis_kelamin': 'laki-laki', + 'foto': self.image, + } + + self.authorize_url = reverse('user-authorize') + + @patch('registrasi.serializers.send_activation_email') + def test_register_and_send_activation_email_is_called_and_user_inactive(self, + mock_ativation_email): + + resp = self.client.post(self.register_url, data=self.register_data) + data = response_decode(resp) + expected = { + 'response': 'User registered, check activation link on your email', + 'email': 'dummy@test.com', + 'name': 'Dummy the boi', + } + + user = User.objects.get(username='dummy@test.com') + + mock_ativation_email.assert_called_once() + self.assertEqual(data, expected) + self.assertFalse(user.is_active) + self.assertEqual(resp.status_code, status.CREATED) + + def test_register_invalid_required_fields(self): + resp = self.client.post(self.register_url, data={}) + data = response_decode(resp) + expected = { + 'name': ['This field is required.'], + 'email': ['This field is required.'], + 'password': ['This field is required.'], + 'tanggal_lahir': ['This field is required.'], + 'jenis_kelamin': ['This field is required.'], + 'phone_number': ['This field is required.'] + } + + self.assertEqual(data, expected) + self.assertEqual(resp.status_code, status.BAD_REQUEST) + + def test_authorize_authenticated(self): + client = get_client_login_with_user(self.user) + resp = client.get(self.authorize_url) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content, b'') + + def test_authorize_not_authenticated(self): + resp = self.client.get(self.authorize_url) + self.assertEqual(resp.status_code, 401) + self.assertEqual(resp.content, + b'{"detail":"Authentication credentials were not provided."}') + + @patch('new_rest_api.views.account_activation_token.check_token', return_value=True) + @patch('new_rest_api.views.force_text') + @patch('new_rest_api.views.urlsafe_base64_decode', return_value='someb64') + @patch('new_rest_api.utils.EmailMessage', return_value=mock_email_message) + @patch('new_rest_api.utils.account_activation_token.make_token', return_value=mock_token) + @patch('new_rest_api.utils.urlsafe_base64_encode', return_value=mock_uid) + def test_send_activation_email_and_activate(self, *args): + self.client.post(self.register_url, data=self.register_data) + + user = User.objects.get(username=self.register_data['email']) + self.assertFalse(user.is_active) + args[4].return_value = user.pk + + activation_url = reverse('user-activate', kwargs={ + 'token': self.mock_token, + 'uidb64': self.mock_uid, + }) + + + resp = self.client.get(activation_url) + data = response_decode(resp) + expected = f'''Aktivasi pengguna bisaGo + +Hai {user.last_name}, +
+
+
+User anda berhasil teraktivasi, terima kasih telah menggunakan bisaGo. +
+
+Salam, +
+bisaGo dev Team +''' + + user.refresh_from_db() + self.assertTrue(user.is_active) + self.assertEqual(resp.status_code, status.OK) + self.assertEqual(data, expected) + + @patch('new_rest_api.views.account_activation_token.check_token', return_value=True) + @patch('new_rest_api.views.force_text') + @patch('new_rest_api.views.urlsafe_base64_decode', return_value='someb64') + @patch('new_rest_api.utils.EmailMessage', return_value=mock_email_message) + @patch('new_rest_api.utils.account_activation_token.make_token', return_value=mock_token) + @patch('new_rest_api.utils.urlsafe_base64_encode', return_value=mock_uid) + def test_send_activation_email_and_activate_user_doesnt_exists(self, *args): + self.client.post(self.register_url, data=self.register_data) + + user = User.objects.get(username=self.register_data['email']) + self.assertFalse(user.is_active) + user_pk = user.pk + 1 + args[4].return_value = user_pk + + activation_url = reverse('user-activate', kwargs={ + 'token': self.mock_token, + 'uidb64': self.mock_uid, + }) + + resp = self.client.get(activation_url) + data = response_decode(resp) + expected = '''Aktivasi pengguna bisaGo + +Terjadi kesalahan atau akun sudah teraktivasi, mohon hubungi tim pengembang bisaGo. +''' + + user.refresh_from_db() + self.assertFalse(user.is_active) + self.assertEqual(resp.status_code, status.OK) + self.assertEqual(data, expected) + + @patch('new_rest_api.views.account_activation_token.check_token', return_value=False) + @patch('new_rest_api.views.force_text') + @patch('new_rest_api.views.urlsafe_base64_decode', return_value='someb64') + @patch('new_rest_api.utils.EmailMessage', return_value=mock_email_message) + @patch('new_rest_api.utils.account_activation_token.make_token', return_value=mock_token) + @patch('new_rest_api.utils.urlsafe_base64_encode', return_value=mock_uid) + def test_send_activation_email_and_activate_token_doesnt_exists(self, *args): + self.client.post(self.register_url, data=self.register_data) + + user = User.objects.get(username=self.register_data['email']) + self.assertFalse(user.is_active) + + other_user = User.objects.create_user(username='other dummy') + args[4].return_value = other_user.pk + + activation_url = reverse('user-activate', kwargs={ + 'token': self.mock_token, + 'uidb64': self.mock_uid, + }) + + resp = self.client.get(activation_url) + data = response_decode(resp) + expected = '''Aktivasi pengguna bisaGo + +Terjadi kesalahan atau akun sudah teraktivasi, mohon hubungi tim pengembang bisaGo. +''' + + user.refresh_from_db() + self.assertFalse(user.is_active) + self.assertEqual(resp.status_code, status.OK) + self.assertEqual(data, expected) diff --git a/new_rest_api/tests.py b/new_rest_api/tests.py deleted file mode 100644 index 81b9256937ad637a0973d9bbbecd9d85ff15785a..0000000000000000000000000000000000000000 --- a/new_rest_api/tests.py +++ /dev/null @@ -1,234 +0,0 @@ -# Create your tests here. -import json -from rest_framework.test import APITestCase, URLPatternsTestCase -from rest_framework import status -from django.urls import include, path, reverse -from django.utils.http import urlsafe_base64_encode -from django.utils.encoding import force_bytes -from registrasi.models import BisaGoUser -from .tokens import account_activation_token -import io -import requests - -class UserTests(APITestCase): - urlpatterns = [ - path('api/', include('new_rest_api.urls')), - ] - - def setUp(self): - url = reverse('create-user') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'password':'chingchenghanji', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu'} - self.client.post(url, data) - - def test_create_user(self): - """ - Ensure we can create a new account object. - """ - url = reverse('create-user') - data = {'name': 'Astray', - 'email':'astrayyahoo@gmail.com', - 'password':'chingchenghanji', - 'phone_number':'08989221856', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'Yuhu', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu', - 'is_active': True} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(BisaGoUser.objects.count(), 2) - user = BisaGoUser.objects.get(phone_number='08989221856').user - self.assertEqual(user.last_name, 'Astray') - - url = reverse('user-details', kwargs={'email':'astrayyahoo@gmail.com'}) - response = self.client.get(url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - json_test = json.loads(response.content) - self.assertEqual(len(json_test), 12) #JSON Attribute - - def test_account_details(self): - url = reverse('user-details', kwargs={'email':'astraykai@gmail.com'}) - response = self.client.get(url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - json_test = json.loads(response.content) - self.assertEqual(len(json_test), 12) #JSON Attribute - - def test_account_list(self): - url = reverse('user-list') - response = self.client.get(url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - json_test = json.loads(response.content) - self.assertEqual(len(json_test), 1) - - def test_incomplete_create_user(self): - url = reverse('create-user') - data = {'email':'astrayyahoo@gmail.com', - 'password':'chingchenghanji', - 'is_active': True} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - json_response = json.loads(response.content) - self.assertEqual(json_response.get('response'), 'bad request. \'name\' key needed') - - def test_user_already_exist(self): - url = reverse('create-user') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'password':'chingchenghanji', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu', - 'is_active': True} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'User is already exist') - - def test_invalid_request(self): - url = reverse('user-list') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'password':'chingchenghanji', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu', - 'is_active': True} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - json.loads(response.content) - - def test_account_login(self): - pass - - def test_without_verification(self): - url = reverse('api-token-auth') - data = {'username': 'astraykai@gmail.com', - 'password':'chingchenghanji'} - response = self.client.post(url, data) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_activation_function(self): - user = BisaGoUser.objects.get(phone_number='089892218567').user - uid = urlsafe_base64_encode(force_bytes(user.pk)) - token = account_activation_token.make_token(user) - url = reverse('activate', kwargs={'uidb64' : uid, 'token' : token}) - response = self.client.get(url) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'User activated') - - def test_update_existing_user(self): - ''' - Ensure that when valid data is sent - It will successfully to update the user - ''' - url = reverse('update-user') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu'} - response = self.client.post(url, data) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'User updated') - - - def test_update_nonexistince_user(self): - ''' - Ensure that when user does not exist in the database - It will failed to update the user - ''' - url = reverse('update-user') - data = {'name': 'Astraykai', - 'email':'astraykaii@gmail.com', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu'} - response = self.client.post(url, data) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'User not found') - - - def test_update_user_with_invalid_data(self): - ''' - Ensure that when invalid data is sent - It will failed to update the user - ''' - url = reverse('update-user') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'phone_number':'', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu'} - response = self.client.post(url, data) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'Internal server error') - - def test_upload_foto_with_invalid_image(self): - ''' - Ensure that profile picture will not be updated if invalid image is sent - ''' - - url = reverse('update-user') - data = {'name': 'Astraykai', - 'email':'astraykai@gmail.com', - 'phone_number':'089892218567', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu', - 'foto':(io.BytesIO(b"this is a test"), 'test.pdf')} - response = self.client.post(url, data, headers={'Content-Type': "multipart/form-data"}) - json_response = json.loads(response.content) - self.assertEqual(json_response['response'], 'File is not image type') - - def test_all_user_information_can_be_seen_after_register(self): - ''' - Ensure that user information can be seen by others after registration - ''' - - url = reverse('create-user') - data = {'name': 'Astray', - 'email':'astrayyahoo@gmail.com', - 'password':'chingchenghanji', - 'phone_number':'08989221856', - 'tanggal_lahir':'1990-05-05', - 'jenis_kelamin':'Laki-laki', - 'disabilitas':'Yuhu', - 'pekerjaan':'Mahasiswa', - 'alamat':'Alamat Palsu', - 'is_active': True} - response = self.client.post(url, data) - - url = reverse('user-details', kwargs={'email':'astrayyahoo@gmail.com'}) - response = self.client.get(url, format='json') - json_test = json.loads(response.content) - print(json_test) - self.assertEqual(json_test['seen'], True) - -class InfoTests(APITestCase, URLPatternsTestCase): - pass diff --git a/new_rest_api/tokens.py b/new_rest_api/tokens.py index 06aee6aa494febae370768bdcc2ea72425cd881d..e8b35d21597dd4fb947e7821371974ce4e368c29 100644 --- a/new_rest_api/tokens.py +++ b/new_rest_api/tokens.py @@ -1,9 +1,13 @@ from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.utils import six + + class TokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): return ( six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active) ) -account_activation_token = TokenGenerator() \ No newline at end of file + + +account_activation_token = TokenGenerator() diff --git a/new_rest_api/urls.py b/new_rest_api/urls.py index f6456d2a95ae27022c9e7ed111e1243b77dc992a..e9b58753f7ea3b6d47b7f133a696680e6ad4ea1d 100644 --- a/new_rest_api/urls.py +++ b/new_rest_api/urls.py @@ -1,14 +1,12 @@ -from django.urls import path -import new_rest_api.views +from django.urls import path, include +from rest_framework.routers import SimpleRouter + +from . import views + +user_router = SimpleRouter() +user_router.register('', views.UserViewSet, basename='user') urlpatterns = [ - path('user-list/', new_rest_api.views.user_list, name='user-list'), - path('user-detail/', new_rest_api.views.user_details, name='user-details'), - path('user-detail/?email=', - new_rest_api.views.user_details, name='user-details-get'), - path('update-user/', new_rest_api.views.update_user, name='update-user'), - path('register/', new_rest_api.views.register_user, name='create-user'), - path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - new_rest_api.views.activate, name='activate'), + path('user/', include(user_router.urls)), ] diff --git a/new_rest_api/utils.py b/new_rest_api/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..819ee5d101bd6503bad183c7e7d331c861b16209 --- /dev/null +++ b/new_rest_api/utils.py @@ -0,0 +1,19 @@ +from django.template.loader import render_to_string +from django.utils.http import urlsafe_base64_encode +from django.utils.encoding import force_bytes +from django.core.mail import EmailMessage +from django.urls import reverse + +from .tokens import account_activation_token + +def send_activation_email(user, request, subject='Activate your account'): + uidb64 = urlsafe_base64_encode(force_bytes(user.pk)) + token = account_activation_token.make_token(user) + absolute_uri = request.build_absolute_uri(reverse('user-activate', + kwargs={'uidb64':uidb64, 'token':token})) + message = render_to_string('acc_active_email.html',{ + 'user': user, + 'absolute_uri': absolute_uri, + }) + mail = EmailMessage(subject, message, to=[user.email]) + mail.send() diff --git a/new_rest_api/views.py b/new_rest_api/views.py index 4e6ec6268ecd1e67f0839b4ea9d7f25d8dfce0f0..b457fdee4d3c2351a354b564d6946ce9e5bfda3f 100644 --- a/new_rest_api/views.py +++ b/new_rest_api/views.py @@ -1,165 +1,63 @@ from http import HTTPStatus as status from django.contrib.auth.models import User -from django.http import JsonResponse, QueryDict -from django.views.decorators.csrf import csrf_exempt +from django.utils.encoding import force_text +from django.utils.http import urlsafe_base64_decode -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.utils.serializer_helpers import ReturnDict - -from django.db.utils import IntegrityError -from django.utils.encoding import force_bytes, force_text -from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode -from django.template.loader import render_to_string -from django.core.mail import EmailMessage -from django.core.exceptions import ObjectDoesNotExist +from rest_framework import viewsets +from rest_framework.authtoken.models import Token +from rest_framework.authentication import TokenAuthentication +from rest_framework.decorators import action +from rest_framework.exceptions import NotAuthenticated +from rest_framework.response import Response from registrasi.models import BisaGoUser -from registrasi.serializers import BisaGoUserSerializers +from registrasi.serializers import BisaGoUserSerializer, RegisterUserSerializer from .tokens import account_activation_token - -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -from django.conf import settings - - -def request_error_message(request_kind): - return "get {} request instead".format(request_kind) - -def missing_key_message(key): - return "bad request. {} key needed".format(key) - - -@csrf_exempt -@api_view(['GET']) -@authentication_classes([]) -@permission_classes([]) -def user_list(request): - if request.method == 'GET': - json_return = [] - for user in BisaGoUser.objects.all(): - json_return.append({"username":user.user.email, - "name": user.user.last_name, - "email": user.user.email, - "phone_number": user.phone_number}) - return JsonResponse(json_return, safe=False, status=status.OK) - else: - return JsonResponse({'response' : request_error_message("get")}, - status=status.METHOD_NOT_ALLOWED) - -@api_view(['GET']) -@authentication_classes([]) -@permission_classes([]) -def user_details(request, email): - try: - if request.method == 'GET': - user = User.objects.get(username=email) - bisa_go_user = BisaGoUser.objects.get(user=user) - serializer = BisaGoUserSerializers(bisa_go_user) - json_return = {"username":user.email, - "name": user.last_name, - "email": user.email, - "phone_number": bisa_go_user.phone_number} - json_return.update(serializer.data) - return JsonResponse(json_return, safe=False, status=status.OK) - else: - return JsonResponse({'response' : request_error_message("get")}, - status=status.METHOD_NOT_ALLOWED) - except ObjectDoesNotExist: - return JsonResponse({'response': 'User not found'}, status=status.NOT_FOUND) - -@api_view(['POST']) -@authentication_classes([]) -@permission_classes([]) -def register_user(request): - try: - if request.method == 'POST': - name = request.POST['name'] - email = request.POST['email'] - password = request.POST['password'] - data = dict(list(request.POST.dict().items())[3:]) - - user = User.objects.create_user(username=email, email=email, - password=password, last_name=name) - user.is_active = False - user.save() - data['user'] = user.pk - data['seen'] = True - mail_subject = "Activate your account" - message = render_to_string('acc_active_email.html', { - 'user' : user, - 'domain' : request.get_host, - 'uid' : urlsafe_base64_encode(force_bytes(user.pk)), - 'token' : account_activation_token.make_token(user), - }) - mail = EmailMessage(mail_subject, message, to=[email]) - mail.send() - data_query_dict = QueryDict('', mutable=True) - data_query_dict.update(data) - serializer = BisaGoUserSerializers(data=data_query_dict) - if serializer.is_valid(): - serializer.save() - return JsonResponse({'response' : 'User created', 'email':email, 'name':name}, - status=status.CREATED) - else: - return JsonResponse(serializer.errors, status=status.BAD_REQUEST) - except KeyError as error: - return JsonResponse({'response' : missing_key_message(str(error))}, - status=status.INTERNAL_SERVER_ERROR) - except IntegrityError as error: - return JsonResponse({'response' : 'User is already exist'}, - status=status.INTERNAL_SERVER_ERROR) - -@api_view(['GET']) -@authentication_classes([]) -@permission_classes([]) -def activate(request, uidb64, token): - if request.method == 'GET': - try: - uid = force_text(urlsafe_base64_decode(uidb64)) - user = User.objects.get(pk=uid) - except(TypeError, ValueError, OverflowError, User.DoesNotExist): - user = None - if user is not None and account_activation_token.check_token(user, token): - user.is_active = True - user.save() - # login(request, user) - # return redirect('home') - return JsonResponse({'response' : 'User activated'}, status=status.CREATED) - else: - return JsonResponse({'response' : request_error_message('get')}, - status=status.BAD_REQUEST) - else: - return JsonResponse({'response' : request_error_message("get")}, status=status.BAD_REQUEST) - -@api_view(['POST']) -@authentication_classes([]) -@permission_classes([]) -def update_user(request): +from .permissions import UserViewPermission +from .renderers import AccountActivationHTMLRenderer + +ACTIVATION_URL_REGEX =\ + r'activate/(?P[0-9A-Za-z_-]+)/(?P[0-9A-Za-z]+-[0-9A-Za-z]{32})' + + +class UserViewSet(viewsets.ModelViewSet): + queryset = BisaGoUser.objects.all() + serializer_class = BisaGoUserSerializer + lookup_field = 'user__username' + lookup_value_regex = '[^/]+' + authentication_classes = (TokenAuthentication,) + permission_classes = (UserViewPermission,) + + @action(detail=False, methods=['POST'], serializer_class=RegisterUserSerializer) + def register(self, request): + data = request.data + serializer = self.get_serializer(data=data, context={'request':request}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + return Response({ + 'response': 'User registered, check activation link on your email', + 'email':instance.email, 'name':instance.last_name}, + status=status.CREATED) + + @action(detail=False, url_path=ACTIVATION_URL_REGEX, + renderer_classes=[AccountActivationHTMLRenderer]) + def activate(self, _, uidb64, token): + return activate(uidb64, token) + + @action(detail=False) + def authorize(self, _): + return Response() + + +def activate(uidb64, token): try: - if request.method == 'POST': - name = request.POST['name'] - email = request.POST['email'] - data = dict(list(request.POST.dict().items())) - if request.FILES : - type = request.FILES['foto'].content_type.split("/")[0] - if type == "image" : - data['foto'] = request.FILES['foto'] - else : - return JsonResponse({'response': 'File is not image type'}, safe=False, - status=status.INTERNAL_SERVER_ERROR) - user = User.objects.get(username=email) - data["user"] = user.pk - bisa_go_user = BisaGoUser.objects.get(user=user) - data_query_dict = QueryDict('', mutable=True) - data_query_dict.update(data) - serializer = BisaGoUserSerializers(bisa_go_user, data=data_query_dict) - if serializer.is_valid(): - user.save() - serializer.save() - else: - return JsonResponse({'response': 'Internal server error'}, safe=False, - status=status.INTERNAL_SERVER_ERROR) - return JsonResponse({'response': 'User updated'}, safe=False, status=status.OK) - except ObjectDoesNotExist: - return JsonResponse({'response': 'User not found'}, status=status.NOT_FOUND) + user_pk = force_text(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=user_pk) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + return Response({'success': 0}) + if account_activation_token.check_token(user, token): + user.is_active = True + user.save() + return Response({'success': 1, 'user': user}) + return Response({'success': 0}) diff --git a/notification/__init__.py b/notification/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notification/apps.py b/notification/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..40b3eb9cd0ad8adf1bd5f2fda51c743e35ed5f6f --- /dev/null +++ b/notification/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NotificationConfig(AppConfig): + name = 'notification' diff --git a/notification/migrations/__init__.py b/notification/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/notification/serializers.py b/notification/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..fb2a80eafbc93eb605bf9a6f29b274a810401411 --- /dev/null +++ b/notification/serializers.py @@ -0,0 +1,29 @@ +from fcm_django.api.rest_framework import FCMDeviceSerializer + + +class NotificationSerializer(FCMDeviceSerializer): + def to_internal_value(self, data): + request = self.context['request'] + data = request.data + fcm_token = data.get('token') + device_type = data.get('type') + parsed_data={ + 'name': request.user.email, + 'active': request.user.is_active, + 'device_id': '%s - %s' % (request.user.email, + fcm_token[:48] if fcm_token else ''), + 'registration_id': fcm_token, + 'type': device_type, + } + return super().to_internal_value(parsed_data) + + def save(self, **kwargs): + instance = super().save(**kwargs) + ModelClass = self.Meta.model + request = self.context['request'] + ModelClass.objects.filter( + registration_id=instance.registration_id, + user=request.user, + active=False, + ).exclude(pk=instance.pk).delete() + return instance diff --git a/notification/test_base.py b/notification/test_base.py new file mode 100644 index 0000000000000000000000000000000000000000..dac147bd57caf256a9fc6ac8a0ef012ca14c3859 --- /dev/null +++ b/notification/test_base.py @@ -0,0 +1,38 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from fcm_django.models import FCMDevice + +from pplbackend.utils import get_client_login_with_user + +class BaseTestNotification(TestCase): + simple_token = 'simple token' + mock_user_test = { + 'username': 'mock username', + 'email': 'self.mock_user@test.com', + 'last_name': 'mock last_name', + } + + mock_notification_device_test = { + 'registration_id': simple_token, + 'type': 'android', + } + + def create_user_test(self, user_dict=mock_user_test): + return User.objects.create_user(**user_dict) + + def create_notification_device_test( + self, + user_dict=mock_user_test, + user=None, + notification_device_dict=mock_notification_device_test + ): + return FCMDevice.objects.create( + **notification_device_dict, + user=self.create_user_test(user_dict=user_dict) if user is None else user + ) + + def setUp(self): + self.user = self.create_user_test() + self.device = self.create_notification_device_test(user=self.user) + self.client = get_client_login_with_user(self.user) diff --git a/notification/test_utils.py b/notification/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d97e0334502a1b03959ecb6d28536563d2c11e41 --- /dev/null +++ b/notification/test_utils.py @@ -0,0 +1,187 @@ +from unittest.mock import patch, MagicMock + +from django.db.models import QuerySet +from django.contrib.auth.models import User +from fcm_django.models import FCMDevice + +from informasi_fasilitas.models import (KomentarFasilitas, KomentarKegiatan, + Fasilitas, Kegiatan, Lokasi) +from . import utils +from .test_base import BaseTestNotification + +class TestUtilsNotification(BaseTestNotification): + + def test_get_type_informasi_from_komentar_should_return_fasilitas(self): + komentar = KomentarFasilitas() + type_fasilitas = utils.get_type_informasi_from_komentar(komentar) + self.assertEqual(type_fasilitas, 'fasilitas') + + def test_get_type_informasi_from_komentarkegiatan_should_return_kegiatan(self): + komentar_kegiatan = KomentarKegiatan() + type_kegiatan = utils.get_type_informasi_from_komentar(komentar_kegiatan) + self.assertEqual(type_kegiatan, 'kegiatan') + + def test_get_informasi_from_komentar_should_return_fasilitas(self): + fasilitas = Fasilitas() + komentar = KomentarFasilitas(informasi=fasilitas) + ret_fasilitas = utils.get_informasi(komentar) + self.assertTrue(isinstance(ret_fasilitas, Fasilitas)) + + def test_get_informasi_from_komentarkegiatan_should_return_kegiatan(self): + kegiatan = Kegiatan() + komentar_kegiatan = KomentarKegiatan(informasi=kegiatan) + ret_kegiatan = utils.get_informasi(komentar_kegiatan) + self.assertTrue(isinstance(ret_kegiatan, Kegiatan)) + + def test_get_place_id_from_komentar_should_return_its_place_id(self): + place_id = 'dummy_place_id' + lokasi = Lokasi(place_id=place_id) + fasilitas = Fasilitas(lokasi=lokasi) + komentar = KomentarFasilitas(informasi=fasilitas) + + ret_place_id = utils.get_place_id_from_komentar(komentar) + + self.assertEqual(ret_place_id, place_id) + + def test_get_place_id_from_komentar_kegiatan_should_return_its_place_id(self): + place_id = 'dummy_place_id' + lokasi = Lokasi(place_id=place_id) + kegiatan = Kegiatan(lokasi=lokasi) + komentar = KomentarKegiatan(informasi=kegiatan) + + ret_place_id = utils.get_place_id_from_komentar(komentar) + + self.assertEqual(ret_place_id, place_id) + + def test_get_informasi_id_from_komentar_should_return_its_informasi_id(self): + _id = 8 + fasilitas = Fasilitas(id=_id) + komentar = KomentarFasilitas(informasi=fasilitas) + + ret_place_id = utils.get_informasi_id_from_komentar(komentar) + + self.assertEqual(ret_place_id, _id) + + def test_get_informasi_id_from_komentar_kegiatan_should_return_its_informasi_id(self): + _id = 9 + kegiatan = Kegiatan(id=_id) + komentar = KomentarKegiatan(informasi=kegiatan) + + ret_place_id = utils.get_informasi_id_from_komentar(komentar) + + self.assertEqual(ret_place_id, _id) + + def test_get_sender_last_name_from_komentar_not_null(self): + user = User(last_name='ariqbasyar') + komentar = KomentarFasilitas(user=user) + + ret_last_name = utils.get_sender_last_name_from_komentar(komentar) + + self.assertEqual(ret_last_name, user.last_name) + + def test_get_target_fcm_device_should_return_its_fasilitas_creator_active_fcmdevice(self): + user = User(username='ariqbasyar') + user.save() + + user_sender = User() + + fcmdevice = FCMDevice(user=user, registration_id='dummy token', type='android') + fcmdevice.save() + + fasilitas = Fasilitas(user=user) + komentar = KomentarFasilitas(informasi=fasilitas, user=user_sender) + + ret_fcm_device = utils.get_target_fcm_device(komentar) + + self.assertEqual(len(ret_fcm_device), 1) + self.assertEqual(ret_fcm_device.first().id, fcmdevice.id) + self.assertEqual(ret_fcm_device.first().registration_id, fcmdevice.registration_id) + self.assertEqual(ret_fcm_device.first().user.username, user.username) + + def test_get_target_fcm_device_should_not_return_its_fasilitas_creator_inactive_fcmdevice(self): + user = User() + user.save() + + username_sender = 'ariqbasyar2' + user_sender = User(username=username_sender) + + fcmdevice = FCMDevice(user=user, registration_id='dummy token', + type='android', active=False) + fcmdevice.save() + + fasilitas = Fasilitas(user=user) + komentar = KomentarFasilitas(informasi=fasilitas, user=user_sender) + + ret_fcm_device = utils.get_target_fcm_device(komentar) + + self.assertEqual(len(ret_fcm_device), 0) + + def test_get_target_fcm_device_should_not_return_its_fasilitas_creator_fcmdevice_if_same_as_komentar_sender(self): + user = User() + user.save() + + fcmdevice = FCMDevice(user=user, registration_id='dummy token', type='android') + fcmdevice.save() + + fasilitas = Fasilitas(user=user) + komentar = KomentarFasilitas(informasi=fasilitas, user=user) + + ret_fcm_device = utils.get_target_fcm_device(komentar) + + self.assertEqual(len(ret_fcm_device), 0) + + def test_get_formatted_message(self): + komentar = MagicMock() + komentar.deskripsi = 'ini deskripsi' + ret_formatted_message = utils.get_formatted_message(komentar) + self.assertEqual(ret_formatted_message, '"%s"' % komentar.deskripsi) + + def test_get_formatted_title(self): + username = 'dummy username' + ret_formatted_title = utils.get_formatted_title(username) + self.assertEqual(ret_formatted_title, 'dummy username menambahkan komentar baru') + + @patch('notification.utils.get_target_fcm_device') + @patch('notification.utils.get_formatted_message') + @patch('notification.utils.get_formatted_title') + @patch('notification.utils.get_sender_last_name_from_komentar') + @patch('notification.utils.get_type_informasi_from_komentar') + @patch('notification.utils.get_informasi_id_from_komentar') + @patch('notification.utils.get_place_id_from_komentar') + def test_send_komentar_notification(self, *args): + komentar = KomentarFasilitas(deskripsi='dummy deskripsi') + fcmdevice_mock = QuerySet(model=FCMDevice) + fcmdevice_mock_return_value = (1,1,0,0,[]) + fcmdevice_mock.send_message = MagicMock(return_value=fcmdevice_mock_return_value) + formatted_title = 'ariqbasyar menambahkan komentar baru' + formatted_message = 'ini komentar' + + args[0].return_value = 'dummy place_id' + args[1].return_value = 1 + args[2].return_value = 'fasilitas' + args[3].return_value = 'ariqbasyar' + args[4].return_value = formatted_title + args[5].return_value = formatted_message + args[6].return_value = fcmdevice_mock + + ret = utils.send_komentar_notification(komentar) + + self.assertEqual(ret, fcmdevice_mock_return_value) + args[0].assert_called_once_with(komentar) + args[1].assert_called_once_with(komentar) + args[2].assert_called_once_with(komentar) + args[3].assert_called_once_with(komentar) + args[4].assert_called_once_with('ariqbasyar') + args[5].assert_called_once_with(komentar) + args[6].assert_called_once_with(komentar) + fcmdevice_mock.send_message.assert_called_once_with( + title=formatted_title, + body=formatted_message, + data={ + 'place_id': 'dummy place_id', + 'id': 1, + 'type': 'fasilitas', + 'title': formatted_title, + 'message': formatted_message, + } + ) diff --git a/notification/test_views.py b/notification/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..a8dc00577883d0e27cd5d6285259224269c935f7 --- /dev/null +++ b/notification/test_views.py @@ -0,0 +1,247 @@ +from http import HTTPStatus +from unittest.mock import patch +from django.urls import reverse +from django.contrib.auth.models import User + +from fcm_django.models import FCMDevice +from fcm_django.api.rest_framework import FCMDeviceSerializer + +from informasi_fasilitas.models import Lokasi, Kegiatan, Fasilitas, KomentarFasilitas, KomentarKegiatan +from pplbackend.utils import get_client_login_with_user, response_decode +from .test_base import BaseTestNotification + +class TestViewsNotification(BaseTestNotification): + def setUp(self): + super().setUp() + self.get_list_notification_device_url = reverse('notification-list') + self.register_notification_url = self.get_list_notification_device_url + + self.user = User.objects.create_user(username='ariqbasyar') + self.komentar_sender_user = \ + User.objects.create_user(username='ariqbasyar2') + self.lokasi = Lokasi.objects.create() + self.fasilitas = Fasilitas.objects.create(lokasi=self.lokasi, + user=self.user, deskripsi='a') + self.kegiatan = Kegiatan.objects.create(lokasi=self.lokasi, + user=self.user, deskripsi='a') + self.kwargs_kegiatan_id = { + 'place_id': self.lokasi.place_id, + 'kegiatan_id' : self.kegiatan.id, + } + + self.add_komentar_kegiatan_url = \ + reverse('add-komentar-kegiatan', kwargs=self.kwargs_kegiatan_id) + + self.kwargs_add_or_list_komentar = { + 'place_id': self.lokasi.place_id, + 'id': self.fasilitas.id, + } + self.add_komentar_url = \ + reverse('add-komentar', kwargs=self.kwargs_add_or_list_komentar) + + self.komentar_client_sender = get_client_login_with_user(self.komentar_sender_user) + + def test_get_list_devices_from_corresponding_user(self): + resp = self.client.get(self.get_list_notification_device_url) + data = response_decode(resp) + + expected = {'count': 1, 'next': None, 'previous': None, 'results': + FCMDeviceSerializer([self.device], many=True).data} + + self.assertEqual(expected, data) + + user2 = self.create_user_test(user_dict={ + 'username': 'mock username2', + 'email': 'self.mock_user2@test.com', + 'last_name': 'mock last_name2', + }) + device2 = self.create_notification_device_test(user=user2, + notification_device_dict={ + 'registration_id': 'simple token 2', + 'type': 'android' + }) + device3 = self.create_notification_device_test(user=user2, + notification_device_dict={ + 'registration_id': 'simple token 3', + 'type': 'android' + }) + + client2 = get_client_login_with_user(user2) + resp2 = client2.get(self.get_list_notification_device_url) + data2 = response_decode(resp2) + + expected2 = {'count': 2, 'next': None, 'previous': None, 'results': + FCMDeviceSerializer([device2, device3], many=True).data} + + self.assertEqual(expected2, data2) + + def test_can_register_device_and_should_return_its_object(self): + FCMDevice.objects.all().delete() + resp = self.client.post(self.register_notification_url, data={ + 'token': 'simple token', + 'type': 'android', + }) + + obj = FCMDevice.objects.all().first() + serializer = FCMDeviceSerializer(obj) + + expected = serializer.data + data = response_decode(resp) + + self.assertEqual(resp.status_code, HTTPStatus.CREATED) + self.assertEqual(data, expected) + + def test_can_register_device_type_web_or_android_or_ios(self): + FCMDevice.objects.all().delete() + + resp1 = self.client.post(self.register_notification_url, data={ + 'token': 'simple token 1', + 'type': 'android', + }) + resp2 = self.client.post(self.register_notification_url, data={ + 'token': 'simple token 2', + 'type': 'ios', + }) + resp3 = self.client.post(self.register_notification_url, data={ + 'token': 'simple token 3', + 'type': 'web', + }) + + self.assertEqual( + list(map(lambda resp: resp.status_code, [resp1, resp2, resp3])), + [HTTPStatus.CREATED]*3) + + def test_cant_register_device_if_device_type_is_not_valid(self): + FCMDevice.objects.all().delete() + _type = 'wearable' + resp = self.client.post(self.register_notification_url, data={ + 'token': 'simple token', + 'type': _type, + }) + + data = response_decode(resp) + expected = {'type': ['"%s" is not a valid choice.' % _type]} + + self.assertEqual(data, expected) + + def test_register_device_duplicate_token_error(self): + FCMDevice.objects.all().delete() + self.client.post(self.register_notification_url, data={ + 'token': 'a', + 'type': 'android', + }) + resp = self.client.post(self.register_notification_url, data={ + 'token': 'a', + 'type': 'android', + }) + + data = response_decode(resp) + expected = {'registration_id': ['This field must be unique.']} + + self.assertEqual(resp.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual(data, expected) + + def test_cant_register_device_if_registration_id_is_null(self): + FCMDevice.objects.all().delete() + resp = self.client.post(self.register_notification_url, data={ + 'type': 'android', + }) + + data = response_decode(resp) + expected = {'registration_id': ['This field may not be null.']} + + self.assertEqual(data, expected) + + def test_cant_register_device_if_registration_id_is_blank(self): + FCMDevice.objects.all().delete() + resp = self.client.post(self.register_notification_url, data={ + 'token': '', + 'type': 'android', + }) + + data = response_decode(resp) + expected = {'registration_id': ['This field may not be blank.']} + + self.assertEqual(data, expected) + + def test_cant_register_device_if_device_type_is_null(self): + FCMDevice.objects.all().delete() + resp = self.client.post(self.register_notification_url, data={ + 'token': 'simple token', + }) + + data = response_decode(resp) + expected = {'type': ['This field may not be null.']} + + self.assertEqual(data, expected) + + def test_set_to_inactive_if_change_account(self): + FCMDevice.objects.all().delete() + user2 = User.objects.create_user(username='another dummy') + client2 = get_client_login_with_user(user2) + data = {'token': 'simple token', 'type': 'android'} + self.client.post(self.register_notification_url, data=data) + resp1 = client2.post(self.register_notification_url, data=data) + resp2 = self.client.post(self.register_notification_url, data=data) + + data1 = response_decode(resp1) + data2 = response_decode(resp2) + + devices = FCMDevice.objects.all() + device1 = devices.get(id=data1['id']) + device2 = devices.get(id=data2['id']) + + self.assertFalse(device1.active) + self.assertTrue(device2.active) + + def test_delete_inactive_device_if_change_account(self): + FCMDevice.objects.all().delete() + user2 = User.objects.create_user(username='another dummy') + client2 = get_client_login_with_user(user2) + data = {'token': 'simple token', 'type': 'android'} + resp1 = self.client.post(self.register_notification_url, data=data) + resp2 = client2.post(self.register_notification_url, data=data) + resp3 = self.client.post(self.register_notification_url, data=data) + + data1 = response_decode(resp1) + data2 = response_decode(resp2) + data3 = response_decode(resp3) + + devices = FCMDevice.objects.all() + device1 = devices.filter(id=data1['id']) + device2 = devices.get(id=data2['id']) + device3 = devices.get(id=data3['id']) + + self.assertFalse(device2.active) + self.assertTrue(device3.active) + self.assertFalse(device1.exists()) + + @patch('informasi_fasilitas.views.send_komentar_notification') + def test_post_komentar_should_call_send_komentar_notification(self, mock_notif): + resp = self.komentar_client_sender.post(self.add_komentar_url, data={ + 'deskripsi': 'dummy deskripsi', + }) + data = response_decode(resp) + komentar = KomentarFasilitas.objects.get(id=data['id']) + + mock_notif.assert_called_once_with(komentar) + + @patch('informasi_fasilitas.views.send_komentar_notification') + def test_failed_to_post_komentar_should_not_call_send_komentar_notification(self, mock_notif): + self.komentar_client_sender.post(self.add_komentar_url, data={}) + mock_notif.assert_not_called() + + @patch('informasi_fasilitas.views_komentar_kegiatan.send_komentar_notification') + def test_post_komentar_kegiatan_should_call_send_komentar_notification(self, mock_notif): + resp = self.komentar_client_sender.post(self.add_komentar_kegiatan_url, data={ + 'deskripsi': 'dummy deskripsi', + }) + data = response_decode(resp) + komentar = KomentarKegiatan.objects.get(id=data['id']) + + mock_notif.assert_called_once_with(komentar) + + @patch('informasi_fasilitas.views_komentar_kegiatan.send_komentar_notification') + def test_failed_to_post_komentar_kegiatan_should_not_call_send_komentar_notification(self, mock_notif): + self.komentar_client_sender.post(self.add_komentar_kegiatan_url, data={}) + mock_notif.assert_not_called() diff --git a/notification/urls.py b/notification/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..ca1353d8d55b019e2562377319342fd031c499f3 --- /dev/null +++ b/notification/urls.py @@ -0,0 +1,11 @@ +from django.urls import path, include +from rest_framework.routers import SimpleRouter + +from . import views + +notification_router = SimpleRouter() +notification_router.register('', views.NotificationViews, basename='notification') + +urlpatterns = [ + path('', include(notification_router.urls)), +] diff --git a/notification/utils.py b/notification/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d3dd9a74ab0b74468882767c3a1478ef3a9d509f --- /dev/null +++ b/notification/utils.py @@ -0,0 +1,53 @@ +from fcm_django.models import FCMDevice + +from informasi_fasilitas.models import KomentarFasilitas + +def get_type_informasi_from_komentar(komentar): + if isinstance(komentar, KomentarFasilitas): + return 'fasilitas' + return 'kegiatan' + +def get_informasi(komentar): + return komentar.informasi + +def get_place_id_from_komentar(komentar): + return get_informasi(komentar).lokasi.place_id + +def get_informasi_id_from_komentar(komentar): + return get_informasi(komentar).id + +def get_sender_last_name_from_komentar(komentar): + return str(komentar.user.last_name) + +def get_target_fcm_device(komentar): + return (FCMDevice.objects + .filter(user=get_informasi(komentar).user, active=True) + .exclude(user=komentar.user)) + +def get_formatted_title(name): + return '%s menambahkan komentar baru' % name + +def get_formatted_message(komentar): + return '"%s"' % komentar.deskripsi + +def send_komentar_notification(komentar): + target_fcm_device = get_target_fcm_device(komentar) + if target_fcm_device: + place_id = get_place_id_from_komentar(komentar) + informasi_id = get_informasi_id_from_komentar(komentar) + informasi_type = get_type_informasi_from_komentar(komentar) + sender = get_sender_last_name_from_komentar(komentar) + formatted_title = get_formatted_title(sender) + formatted_message = get_formatted_message(komentar) + + return target_fcm_device.send_message( + title=formatted_title, + body=formatted_message, + data={ + 'place_id': place_id, + 'id': informasi_id, + 'type': informasi_type, + 'title': formatted_title, + 'message': formatted_message, + } + ) diff --git a/notification/views.py b/notification/views.py new file mode 100644 index 0000000000000000000000000000000000000000..67ccfbda4d6fba4ded8941e445152fe994f16d4a --- /dev/null +++ b/notification/views.py @@ -0,0 +1,9 @@ +from rest_framework.authentication import TokenAuthentication +from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet + +from .serializers import NotificationSerializer + + +class NotificationViews(FCMDeviceAuthorizedViewSet): + authentication_classes = (TokenAuthentication,) + serializer_class = NotificationSerializer diff --git a/oauth/tests.py b/oauth/tests.py index 18fa16625621b61c8c41a0c0d640a9a23381bedb..931de91789bb8e4e157f6e8f9ba5839630d96336 100644 --- a/oauth/tests.py +++ b/oauth/tests.py @@ -1,12 +1,15 @@ -from django.test import TestCase import json -from .views import validate_google_token, _create_random_phone_number, _create_google_user + from unittest.mock import patch +from django.test import TestCase from django.test import Client -from registrasi.models import BisaGoUser from django.contrib.auth.models import User from rest_framework.exceptions import AuthenticationFailed +from pplbackend.utils import response_decode +from registrasi.models import BisaGoUser +from .views import validate_google_token, _create_random_phone_number, _create_google_user + class TestOauth(TestCase): def setUp(self): @@ -41,9 +44,10 @@ class TestOauth(TestCase): "access_token": "sankdsanlk", 'password': passcode }) - json_response = json.loads(response.content) + json_response = response_decode(response) + user = json_response.get('user') self.assertEqual(200, response.status_code) - self.assertEqual('mock_user@email.com', json_response.get("username")) + self.assertEqual('mock_user@email.com', user.get("username")) def test_request_token_email_not_exists(self): email = 'mock_user1212@email.com' @@ -96,9 +100,10 @@ class TestOauth(TestCase): 'password': passcode, 'google': True, }) - json_response = json.loads(response.content) + json_response = response_decode(response) + user = json_response.get('user') self.assertEqual(200, response.status_code) - self.assertEqual('mock_user@email.com', json_response.get("username")) + self.assertEqual('mock_user@email.com', user.get("username")) @patch('oauth.views.json.loads') @@ -113,9 +118,10 @@ class TestOauth(TestCase): 'password': passcode, 'google': True, }) - json_response = json.loads(response.content) + json_response = response_decode(response) + user = json_response.get('user') self.assertEqual(200, response.status_code) - self.assertEqual('mock_user4545@email.com', json_response.get("username")) + self.assertEqual('mock_user4545@email.com', user.get("username")) @patch('oauth.views.json.loads') def test_google_login_error(self, mock_json_loads): @@ -175,4 +181,4 @@ class TestOauth(TestCase): self.assertEqual('mock_user3434@email.com', user.username) self.assertEqual('mock_user3434@email.com', user.email) self.assertEqual('name', user.last_name) - self.assertEqual('1x1x1x1x1x1x1x1', user.phone_number.phone_number) + self.assertEqual('1x1x1x1x1x1x1x1', user.bisagouser.phone_number) diff --git a/oauth/views.py b/oauth/views.py index d516f8650c3d7f446fb9504d1da79fb2a0539601..e9e943f632eb86dfe511382cfc9bac2518c15252 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -11,9 +11,9 @@ from django.contrib.auth.base_user import BaseUserManager from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User from django.conf import settings -from registrasi.models import BisaGoUser - +from registrasi.models import BisaGoUser +from registrasi.serializers import BisaGoUserSerializer @csrf_exempt @api_view(http_method_names=['POST']) @@ -30,8 +30,7 @@ def request_token(request): result = _check_normal_auth(request, email, password) user = result - return _check_user(user) - + return _check_user(user, request) def _google_check(access_token, name): try: @@ -44,7 +43,6 @@ def _google_check(access_token, name): user = _create_google_user(email=result, name=name) return user - def _check_normal_auth(request, email, password): try: user = authenticate(request, username=email, password=password) @@ -55,16 +53,16 @@ def _check_normal_auth(request, email, password): raise NotFound(detail="User doesn't exist") return user - -def _check_user(user): +def _check_user(user, request): if user.is_active: - token, create = Token.objects.get_or_create(user=user) - response = {'username': user.username, 'token': token.key, 'token_type': "token"} + token, _ = Token.objects.get_or_create(user=user) + request.user = user + serializer = BisaGoUserSerializer(user.bisagouser, context={'request':request}) + response = {'user':serializer.data, 'token': token.key, 'token_type': "token"} return JsonResponse(response, status=200) else: raise AuthenticationFailed(detail="Please activate your account") - def _create_google_user(email, name): try: return User.objects.get(username=email) @@ -82,7 +80,6 @@ def _create_google_user(email, name): BisaGoUser.objects.create(user=user, phone_number=random_generated_phone_number) return user - def _create_random_phone_number(): phone_number = 'x'.join([str(random.randint(0, 9)) for _ in range(8)]) try: @@ -91,7 +88,6 @@ def _create_random_phone_number(): except BisaGoUser.DoesNotExist: return phone_number - def validate_google_token(access_token): payload = {'access_token': access_token} # validate the token req = requests.get('https://www.googleapis.com/oauth2/v2/userinfo', @@ -99,12 +95,4 @@ def validate_google_token(access_token): data = json.loads(req.text) if 'error' in data or 'email' not in data: raise AuthenticationFailed(detail='Wrong google token / this google token is already expired.') - return data.get("email") - - - - - - - - + return data.get("email") \ No newline at end of file diff --git a/pplbackend/custom_model_field.py b/pplbackend/custom_model_field.py new file mode 100644 index 0000000000000000000000000000000000000000..407ea3487dc04a626ed65d473d82682a1285dbd6 --- /dev/null +++ b/pplbackend/custom_model_field.py @@ -0,0 +1,38 @@ +from io import BytesIO +import os +from django.core.files import File +from django.db import models +from django.db.models.fields.files import ImageFieldFile +from PIL import Image, ImageOps + + +class CompressedImageFieldFile(ImageFieldFile): + def save(self, name, content, save=True): + # Compressed Image + image = Image.open(content) + image = image.convert('RGB') + image = ImageOps.exif_transpose(image) + im_io = BytesIO() + image.save(im_io, "JPEG", optimize=True, quality=self.field.quality) + + # Change extension + filename = os.path.splitext(name)[0] + filename = f"{filename}.jpg" + + image = File(im_io, name=filename) + super().save(filename, image, save) + + +class CompressedImageField(models.ImageField): + attr_class = CompressedImageFieldFile + + def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, + quality=90, **kwargs): + self.quality = quality + super().__init__(verbose_name, name, width_field, height_field, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.quality: + kwargs['quality'] = self.quality + return name, path, args, kwargs diff --git a/pplbackend/settings.py b/pplbackend/settings.py index 1804a87c224195e2e824954edec195a212d1f154..20beac1b48bca36c6570a57ee0af6831c71863d8 100644 --- a/pplbackend/settings.py +++ b/pplbackend/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ 'informasi_fasilitas', 'new_rest_api', 'layanan_khusus', + 'notification', 'rest_auth', 'rest_framework', @@ -58,6 +59,7 @@ INSTALLED_APPS = [ 'allauth.socialaccount.providers.google', 'multiselectfield', 'oauth2_provider', + 'fcm_django', ] MIDDLEWARE = [ @@ -200,7 +202,7 @@ SOCIALACCOUNT_PROVIDERS = { LOGIN_REDIRECT_URL = '/' REST_FRAMEWORK = { - 'DATETIME_FORMAT': "%Y-%m-%d %H:%M", + 'DATETIME_FORMAT': "%Y-%m-%d %H:%M%z", 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, 'DEFAULT_AUTHENTICATION_CLASSES': [ @@ -243,3 +245,17 @@ PROXIES = { "https": https_proxy } MAP_API_KEY = os.environ.get('MAP_API_KEY', '') + +FCM_DJANGO_SETTINGS = { + # default: _('FCM Django') + "APP_VERBOSE_NAME": os.getenv('APP_VERBOSE_NAME', 'BisaGo Notification'), + # Your firebase API KEY + "FCM_SERVER_KEY": os.getenv('FCM_SERVER_KEY'), + # true if you want to have only one active device per registered user at a time + # default: False + "ONE_DEVICE_PER_USER": os.getenv('ONE_DEVICE_PER_USER') == 'True', + # devices to which notifications cannot be sent, + # are deleted upon receiving error response from FCM + # default: False + "DELETE_INACTIVE_DEVICES": os.getenv('DELETE_INACTIVE_DEVICES') == 'True', +} diff --git a/pplbackend/urls.py b/pplbackend/urls.py index 835ffdc77e702e444add522f08b257c1f70a6aea..eae0351a279d82517a7bba07af597ac63a358490 100644 --- a/pplbackend/urls.py +++ b/pplbackend/urls.py @@ -42,4 +42,5 @@ urlpatterns = [ path('request-token/', views.obtain_auth_token, name='token-request-auth'), #path('api-token-auth/', views.obtain_auth_token, name='api-token-auth'), path('informasi-fasilitas/', include('informasi_fasilitas.urls')), + path('notification/', include('notification.urls')), path('layanan-khusus/', include('layanan_khusus.urls'))] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/pplbackend/utils.py b/pplbackend/utils.py index 1273caff9f5d28206030d7b79bef99432c260406..b9157964480ef95d93c5efc49f193cb86abde99b 100644 --- a/pplbackend/utils.py +++ b/pplbackend/utils.py @@ -14,11 +14,10 @@ def get_client_login_with_user(user): return client -def response_decode( - response, -): +def response_decode(response): + content = response.content.decode('utf-8') try: - content = json.loads(response.content.decode('utf-8')) - except json.decoder.JSONDecodeError as json_error: - return None + return json.loads(content) + except json.decoder.JSONDecodeError: + pass return content diff --git a/registrasi/migrations/0006_merge_20210519_1213.py b/registrasi/migrations/0006_merge_20210519_1213.py new file mode 100644 index 0000000000000000000000000000000000000000..4de0e03b1b350829d7f7615be1d40896c03e8ef9 --- /dev/null +++ b/registrasi/migrations/0006_merge_20210519_1213.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-19 12:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0005_auto_20210516_1734'), + ('registrasi', '0005_bisagouser_foto'), + ] + + operations = [ + ] diff --git a/registrasi/migrations/0008_auto_20210525_1756.py b/registrasi/migrations/0008_auto_20210525_1756.py new file mode 100644 index 0000000000000000000000000000000000000000..3422383d61ff2dfd3ad0c36139a93300b256b11c --- /dev/null +++ b/registrasi/migrations/0008_auto_20210525_1756.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-25 17:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0007_merge_20210517_0447'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='foto', + field=models.ImageField(blank=True, default=None, null=True, upload_to='', verbose_name='Foto'), + ), + ] diff --git a/registrasi/migrations/0008_merge_20210527_2250.py b/registrasi/migrations/0008_merge_20210527_2250.py new file mode 100644 index 0000000000000000000000000000000000000000..9730e9cee1cb1b2468358963ef6227caa94b75be --- /dev/null +++ b/registrasi/migrations/0008_merge_20210527_2250.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-27 22:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0007_merge_20210517_0447'), + ('registrasi', '0006_merge_20210519_1213'), + ] + + operations = [ + ] diff --git a/registrasi/migrations/0009_bisagouser_organisasi_komunitas.py b/registrasi/migrations/0009_bisagouser_organisasi_komunitas.py new file mode 100644 index 0000000000000000000000000000000000000000..c94e3ac94b34aa8ebaa2f085d94db2e052bb61f8 --- /dev/null +++ b/registrasi/migrations/0009_bisagouser_organisasi_komunitas.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-26 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0008_auto_20210525_1756'), + ] + + operations = [ + migrations.AddField( + model_name='bisagouser', + name='organisasi_komunitas', + field=models.CharField(default='-', max_length=64, null=True, verbose_name='Organisasi / Komunitas'), + ), + ] diff --git a/registrasi/migrations/0010_auto_20210527_0722.py b/registrasi/migrations/0010_auto_20210527_0722.py new file mode 100644 index 0000000000000000000000000000000000000000..8f03bc64b53758d8992037bea44a7e861e9e3f8c --- /dev/null +++ b/registrasi/migrations/0010_auto_20210527_0722.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-27 07:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0009_bisagouser_organisasi_komunitas'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='foto', + field=models.ImageField(blank=True, default=None, null=True, upload_to='user/', verbose_name='Foto'), + ), + ] diff --git a/registrasi/migrations/0011_merge_20210528_1510.py b/registrasi/migrations/0011_merge_20210528_1510.py new file mode 100644 index 0000000000000000000000000000000000000000..c5ab54b698b7ce2a51075134162527dd853f3862 --- /dev/null +++ b/registrasi/migrations/0011_merge_20210528_1510.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-28 15:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0010_auto_20210527_0722'), + ('registrasi', '0008_merge_20210527_2250'), + ] + + operations = [ + ] diff --git a/registrasi/migrations/0012_auto_20210528_1519.py b/registrasi/migrations/0012_auto_20210528_1519.py new file mode 100644 index 0000000000000000000000000000000000000000..5d27ec427893b07f678e590e1311a353d7668e17 --- /dev/null +++ b/registrasi/migrations/0012_auto_20210528_1519.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.7 on 2021-05-28 15:19 + +from django.db import migrations +import pplbackend.custom_model_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0011_merge_20210528_1510'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='foto', + field=pplbackend.custom_model_field.CompressedImageField(blank=True, default=None, null=True, quality=55, upload_to='user/', verbose_name='Foto'), + ), + ] diff --git a/registrasi/migrations/0013_auto_20210601_0748.py b/registrasi/migrations/0013_auto_20210601_0748.py new file mode 100644 index 0000000000000000000000000000000000000000..391e57df575814fb9f68fc5638e286de07e634c9 --- /dev/null +++ b/registrasi/migrations/0013_auto_20210601_0748.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-06-01 07:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0012_auto_20210528_1519'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='organisasi_komunitas', + field=models.CharField(blank=True, default='-', max_length=64, null=True, verbose_name='Organisasi / Komunitas'), + ), + ] diff --git a/registrasi/migrations/0014_auto_20210601_0849.py b/registrasi/migrations/0014_auto_20210601_0849.py new file mode 100644 index 0000000000000000000000000000000000000000..4413c5d277981ce9bbd87a0e2ea5591b8373241c --- /dev/null +++ b/registrasi/migrations/0014_auto_20210601_0849.py @@ -0,0 +1,28 @@ +# Generated by Django 3.1.7 on 2021-06-01 08:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0013_auto_20210601_0748'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='alamat', + field=models.CharField(blank=True, default='-', max_length=255, null=True, verbose_name='Alamat'), + ), + migrations.AlterField( + model_name='bisagouser', + name='disabilitas', + field=models.CharField(blank=True, default='Belum memilih', max_length=64, null=True, verbose_name='Disabilitas'), + ), + migrations.AlterField( + model_name='bisagouser', + name='pekerjaan', + field=models.CharField(blank=True, default='-', max_length=64, null=True, verbose_name='Pekerjaan'), + ), + ] diff --git a/registrasi/migrations/0015_auto_20210601_1755.py b/registrasi/migrations/0015_auto_20210601_1755.py new file mode 100644 index 0000000000000000000000000000000000000000..45b788bffd1d43ad9a6013eec4c47ad6f220fe95 --- /dev/null +++ b/registrasi/migrations/0015_auto_20210601_1755.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.7 on 2021-06-01 17:55 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('registrasi', '0014_auto_20210601_0849'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='bisagouser', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/registrasi/models.py b/registrasi/models.py index 3363a3fd043177f80ddca0161e7a0127e534872c..9867d9cdd0cec8a7f5f9bba64be787c02948f0b3 100644 --- a/registrasi/models.py +++ b/registrasi/models.py @@ -1,20 +1,25 @@ +from datetime import date + from django.db import models -from django.core.mail import send_mail from django.contrib.auth.models import User -from datetime import date + +from pplbackend.custom_model_field import CompressedImageField class BisaGoUser(models.Model): - class Meta: - db_table = "BisaGoUser" - user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="phone_number") + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="bisagouser") phone_number = models.CharField('Nomor Telepon', max_length=15, unique=True) tanggal_lahir = models.DateField('Tanggal Lahir', max_length=15, null=False, default=date.today) jenis_kelamin = models.CharField('Jenis Kelamin', max_length=20, default='-') - disabilitas = models.CharField('Disabilitas', max_length=64, null=True, default='Belum memilih') - pekerjaan = models.CharField('Pekerjaan', max_length=64, default='-') - alamat = models.CharField('Alamat', max_length=255, default='-') - foto = models.ImageField('Foto', blank=True, default='-', null=True) + disabilitas = models.CharField('Disabilitas', max_length=64, null=True, blank=True, default='Belum memilih') + organisasi_komunitas = models.CharField('Organisasi / Komunitas', max_length=64, blank=True, null=True, default='-') + pekerjaan = models.CharField('Pekerjaan', max_length=64, blank=True, null=True, default='-') + alamat = models.CharField('Alamat', max_length=255, blank=True, null=True, default='-') + foto = CompressedImageField('Foto', upload_to='user/', blank=True, null=True, default=None, quality=55) seen = models.BooleanField('Seen', blank=True, default=True) + + class Meta: + db_table = "BisaGoUser" + def __str__(self): - return self.user.username \ No newline at end of file + return self.user.username diff --git a/registrasi/serializers.py b/registrasi/serializers.py index addcd1db33fde00526054fa7c342cc6070b633f6..792ad232866b90d951e0a95905e8037cbf31467e 100644 --- a/registrasi/serializers.py +++ b/registrasi/serializers.py @@ -1,21 +1,180 @@ from rest_framework import serializers -from django.db import models +from colour import Color + +from django.contrib.auth.models import User +from django.utils.regex_helper import _lazy_re_compile + +from new_rest_api.utils import send_activation_email from .models import BisaGoUser -class BisaGoUserSerializers(serializers.ModelSerializer): + +class BisaGoUserSerializer(serializers.ModelSerializer): + username = serializers.CharField(source='user.username', read_only=True) + name = serializers.CharField(source='user.last_name') + email = serializers.CharField(source='user.email', read_only=True) + tanggal_lahir = serializers.DateField(format='%d %B %Y', + input_formats=['%d %B %Y', '%Y-%m-%d']) + + hidden_fields = serializers.SerializerMethodField() + hidden_fields_verbose = serializers.SerializerMethodField() + can_see_hidden_fields = serializers.SerializerMethodField() + hidden_fields_color = serializers.SerializerMethodField() + class Meta: model = BisaGoUser fields = ( - 'user', - 'phone_number', - 'tanggal_lahir', - 'jenis_kelamin', - 'disabilitas', - 'pekerjaan', - 'alamat', - 'foto', - 'seen', + 'phone_number', 'tanggal_lahir', 'jenis_kelamin', 'name', 'email', + 'disabilitas', 'pekerjaan', 'alamat', 'foto', 'seen', 'username', + 'organisasi_komunitas', 'hidden_fields', 'hidden_fields_verbose', + 'can_see_hidden_fields', 'hidden_fields_color' ) - extra_kwargs = { - 'foto': {'required': False}, - } \ No newline at end of file + + hidden_fields = ('phone_number', 'email', 'alamat', 'tanggal_lahir') + hidden_fields_verbose = { + 'phone_number': 'nomor telepon', + 'tanggal_lahir': 'tanggal lahir', + } + hidden_replacement_char = '({} dirahasiakan)' + + update_bisago_fields = ( + 'phone_number', 'seen', 'alamat', 'jenis_kelamin', 'tanggal_lahir', + 'disabilitas', 'pekerjaan', 'foto', 'organisasi_komunitas' + ) + # example mapping field 'model_field' from 'input_field' + # {'model_field':'input_field'} + # needed when field from model is difference from input data + # see actual example on RegisterUserSerializer.Meta.create_user_fields_mapper + update_bisago_fields_mapper = {} + + update_user_fields = ('last_name',) + update_user_fields_mapper = {} + + def get_hidden_fields(self, *_): + return self.Meta.hidden_fields + + def get_hidden_fields_verbose(self, *_): + hidden_fields = self.Meta.hidden_fields + verbose = self.Meta.hidden_fields_verbose + return [verbose.get(field) or field for field in hidden_fields] + + def get_can_see_hidden_fields(self, instance): + return self._can_see_hidden_fields(instance) + + def get_hidden_fields_color(self, *_): + return Color('#808080').hex_l + + def _can_see_hidden_fields(self, instance): + request = self.context['request'] + return request.user.id == instance.user.id or instance.seen + + def to_representation(self, instance): + representation = super().to_representation(instance) + if not self._can_see_hidden_fields(instance): + for hidden_field in self.Meta.hidden_fields: + verbose_name = self.Meta.hidden_fields_verbose.get(hidden_field)\ + or hidden_field + representation[hidden_field] = self.Meta.hidden_replacement_char\ + .format(verbose_name) + return representation + + def to_internal_value(self, data): + copied_data = data.copy() + if copied_data.get('foto') == '': + if self.instance.foto is not None: + copied_data['foto'] = self.instance.foto + internal_value = super().to_internal_value(copied_data) + return internal_value + + def _update_instance(self, instance, validated_data, fields, mapper): + for field in fields: + mapped = mapper.get(field) or field + setattr(instance, field, validated_data.get(mapped)) + return instance + + def update(self, instance, validated_data): + instance = self._update_instance(instance, validated_data, + self.Meta.update_bisago_fields, self.Meta.update_bisago_fields_mapper) + instance.save() + + user = self._update_instance(instance.user, validated_data.get('user'), + self.Meta.update_user_fields, self.Meta.update_user_fields_mapper) + user.save() + return instance + + +class RegisterUserSerializer(serializers.ModelSerializer): + phone_regex = _lazy_re_compile('^(?:[+0]9)?[0-9]{11,12}$') + + name = serializers.CharField() + email = serializers.EmailField() + phone_number = serializers.CharField() + jenis_kelamin = serializers.CharField() + tanggal_lahir = serializers.DateField(format='%Y-%m-%d') + password = serializers.CharField(style={'input_type': 'password'}, + write_only=True) + + disabilitas = serializers.CharField(required=False) + organisasi_komunitas = serializers.CharField(required=False) + pekerjaan = serializers.CharField(required=False) + alamat = serializers.CharField(required=False) + foto = serializers.ImageField(required=False) + + class Meta: + model = User + fields = ( + 'name', 'email', 'password', 'tanggal_lahir', 'jenis_kelamin', + 'disabilitas', 'pekerjaan', 'alamat', 'phone_number', 'foto', + 'organisasi_komunitas', + ) + + create_user_fields = ( + 'username', 'email', 'password', 'last_name', 'is_active', + ) + create_user_fields_mapper = { + 'username': 'email', 'last_name': 'name', + } + + create_bisago_fields = ( + 'phone_number', 'tanggal_lahir', 'jenis_kelamin', 'disabilitas', + 'pekerjaan', 'alamat', 'foto', 'user', 'organisasi_komunitas', + ) + create_bisago_fields_mapper = {} + + def validate_email(self, value): + if User.objects.filter(username=value).exists(): + raise serializers.ValidationError('email already exists.') + return value + + def validate_phone_number(self, value): + matches = self.phone_regex.search(value) + if not matches: + raise serializers.ValidationError('invalid phone number.') + if BisaGoUser.objects.filter(phone_number=value).exists(): + raise serializers.ValidationError('phone number already exists.') + return value + + def _create_model(self, creator_manager, validated_data, fields, mapper): + instance_data = {} + for field in fields: + mapped = mapper.get(field) or field + value = validated_data.get(mapped) + if value is not None: + instance_data[field] = value + return creator_manager(**instance_data) + + def create(self, validated_data): + validated_data['is_active'] = False + user = self._create_model(User.objects.create_user, validated_data, + self.Meta.create_user_fields, self.Meta.create_user_fields_mapper) + + validated_data['user'] = user + self._create_model(BisaGoUser.objects.create, validated_data, + self.Meta.create_bisago_fields, self.Meta.create_bisago_fields_mapper) + + try: + request = self.context['request'] + send_activation_email(user, request) + except ConnectionError: + print('Failed to send activation email to %s, connection error.' %\ + (user.username)) + return user diff --git a/registrasi/test_models.py b/registrasi/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..ec7a069e3bfaacdc02767859091955fe57f80a79 --- /dev/null +++ b/registrasi/test_models.py @@ -0,0 +1,14 @@ +from django.contrib.auth.models import User +from django.test import TestCase + +from .models import BisaGoUser + + +class TestModelsBisaGoUser(TestCase): + def test_str(self): + user = User.objects.create( + username='dummy@test.com', last_name='dummy lastname') + bisagouser = BisaGoUser.objects.create(user=user, + phone_number='000011112222') + + self.assertEqual(str(bisagouser), user.username) diff --git a/registrasi/test_serializers.py b/registrasi/test_serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..06a9b354a99f0976c762cb7dc91462604c125a12 --- /dev/null +++ b/registrasi/test_serializers.py @@ -0,0 +1,356 @@ +from unittest.mock import patch + +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from rest_framework import serializers + +from .serializers import BisaGoUserSerializer, RegisterUserSerializer +from .models import BisaGoUser + + +class MockUser: + def __init__(self, id=1): + self.id = id + + +class MockRequest: + def __init__(self, user_id=1): + self.user = MockUser(id=user_id) + + def build_absolute_uri(self, path): + return 'http:/dummy%s' % path + + +class MockEmptyImage: + def __init__(self, name, size=1): + self.name = name + self.size = size + # https://stackoverflow.com/a/26896684/11485041 + self.content = b"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" + self._committed = True + + def __eq__(self, obj): + if isinstance(obj, str): + if obj == '': + return True + + +class BaseTestSerializer(TestCase): + mock_user = { + 'username': 'dummy username', + 'last_name': 'dummy lastname', + 'email': 'test@email.com', + } + mock_bisagouser = { + 'phone_number': '081208120812', + 'alamat': 'Dummy St', + 'tanggal_lahir': '2000-01-01', + } + + def setUp(self): + self.image = open("test_file/test1.jpg", 'rb') + self.upload_image = SimpleUploadedFile("test1.jpg", + content=self.image.read(), content_type='upload_image/jpeg') + self.hidden_fields = ('phone_number','email','alamat','tanggal_lahir') + self.hidden_fields_verbose = ['nomor telepon','email','alamat','tanggal lahir'] + self.maxDiff = 1e5 + +class TestBisaGoUserSerializer(BaseTestSerializer): + def test_meta_model(self): + self.assertEqual(BisaGoUserSerializer.Meta.model, BisaGoUser) + + def test_meta_fields(self): + self.assertSequenceEqual(BisaGoUserSerializer.Meta.fields, ( + 'phone_number', 'tanggal_lahir', 'jenis_kelamin', 'name', 'email', + 'disabilitas', 'pekerjaan', 'alamat', 'foto', 'seen', 'username', + 'organisasi_komunitas', 'hidden_fields', 'hidden_fields_verbose', + 'can_see_hidden_fields', 'hidden_fields_color', + )) + + def test_meta_hidden_fields(self): + self.assertEqual(BisaGoUserSerializer.Meta.hidden_fields, + ('phone_number', 'email', 'alamat', 'tanggal_lahir')) + + def test_meta_hidden_fields_verbose(self): + self.assertEqual(BisaGoUserSerializer.Meta.hidden_fields_verbose, + {'phone_number': 'nomor telepon', 'tanggal_lahir': 'tanggal lahir'}) + + def test_meta_hidden_replacement_char(self): + self.assertEqual(BisaGoUserSerializer.Meta.hidden_replacement_char, + '({} dirahasiakan)') + + def test_meta_update_bisago_fields(self): + self.assertEqual(BisaGoUserSerializer.Meta.update_bisago_fields, ( + 'phone_number', 'seen', 'alamat', 'jenis_kelamin', 'tanggal_lahir', + 'disabilitas', 'pekerjaan', 'foto', 'organisasi_komunitas' + )) + + def test_meta_update_bisago_fields_mapper(self): + self.assertDictEqual(BisaGoUserSerializer.Meta.update_bisago_fields_mapper, {}) + + def test_meta_update_user_fields(self): + self.assertEqual(BisaGoUserSerializer.Meta.update_user_fields, ('last_name',)) + + def test_meta_update_user_fields_mapper(self): + self.assertEqual(BisaGoUserSerializer.Meta.update_user_fields_mapper, {}) + + def test_can_see_hidden_fields_obj_seen_is_true_from_others(self): + user = User.objects.create_user(**self.mock_user) + bisagouser = BisaGoUser.objects.create(user=user, **self.mock_bisagouser) + + user_id = 99 + assert user.id != user_id + request = MockRequest(user_id=user_id) + bisagouser = BisaGoUserSerializer(bisagouser, + context={'request':request}) + + expected = { + 'phone_number': '081208120812', 'tanggal_lahir': '2000-01-01', + 'jenis_kelamin': '-', 'disabilitas': 'Belum memilih', + 'pekerjaan': '-', 'alamat': '-', 'foto': None, 'seen': True, + 'username': 'dummy username', 'name': 'dummy lastname', + 'email': 'test@email.com', 'alamat': 'Dummy St', + 'organisasi_komunitas': '-', 'hidden_fields': self.hidden_fields, + 'hidden_fields_verbose': self.hidden_fields_verbose, + 'can_see_hidden_fields': True, 'hidden_fields_color': '#808080', + } + + self.assertEqual(bisagouser.data, expected) + + def test_can_see_hidden_fields_obj_seen_is_true_from_itself(self): + user = User.objects.create_user(**self.mock_user) + bisagouser = BisaGoUser.objects.create(user=user, **self.mock_bisagouser) + + request = MockRequest(user_id=user.id) + bisagouser = BisaGoUserSerializer(bisagouser, + context={'request':request}) + + expected = { + 'phone_number': '081208120812', 'tanggal_lahir': '2000-01-01', + 'jenis_kelamin': '-', 'disabilitas': 'Belum memilih', + 'pekerjaan': '-', 'alamat': 'Dummy St', 'foto': None, 'seen': True, + 'username': 'dummy username', 'name': 'dummy lastname', + 'email': 'test@email.com', 'organisasi_komunitas': '-', + 'hidden_fields': self.hidden_fields, + 'hidden_fields_verbose': self.hidden_fields_verbose, + 'can_see_hidden_fields': True, 'hidden_fields_color': '#808080', + } + + self.assertEqual(bisagouser.data, expected) + + def test_cant_see_hidden_fields_obj_seen_is_false_from_other(self): + user = User.objects.create_user(**self.mock_user) + bisagouser = BisaGoUser.objects.create(user=user, + **self.mock_bisagouser, seen=False) + + user_id = 99 + assert user_id != user.id + request = MockRequest(user_id=user_id) + bisagouser = BisaGoUserSerializer(bisagouser, + context={'request':request}) + + expected = { + 'phone_number': '(nomor telepon dirahasiakan)', + 'tanggal_lahir': '(tanggal lahir dirahasiakan)', + 'jenis_kelamin': '-', 'disabilitas': 'Belum memilih', + 'pekerjaan': '-', 'foto': None, 'seen': False, + 'username': 'dummy username', 'name': 'dummy lastname', + 'email': '(email dirahasiakan)', 'alamat': '(alamat dirahasiakan)', + 'organisasi_komunitas': '-', 'hidden_fields': self.hidden_fields, + 'hidden_fields_verbose': self.hidden_fields_verbose, + 'can_see_hidden_fields': False, 'hidden_fields_color': '#808080', + } + + self.assertDictEqual(bisagouser.data, expected) + + def test_can_update(self): + user = User.objects.create_user(**self.mock_user) + bisagouser = BisaGoUser.objects.create(user=user, + **self.mock_bisagouser) + + update = { + 'name': 'another dummy name', 'phone_number': '99999999', + 'seen': False, 'alamat': 'dif dummy st', 'foto': self.upload_image, + 'jenis_kelamin': 'laki-laki', 'tanggal_lahir': '2000-10-02', + 'disabilitas': 'tanpa batas', 'pekerjaan': 'makan', + 'organisasi_komunitas': 'komunitas tanpa batas' + } + request = MockRequest(user_id=user.id) + + serializer = BisaGoUserSerializer(bisagouser, data=update, + context={'request':request}) + serializer.is_valid(raise_exception=True) + serializer.save() + + expected = { + 'phone_number': '99999999', 'tanggal_lahir': '02 October 2000', + 'jenis_kelamin': 'laki-laki', 'disabilitas': 'tanpa batas', + 'pekerjaan': 'makan', 'alamat': 'dif dummy st', 'seen': False, + 'username': 'dummy username', 'name': 'another dummy name', + 'email': 'test@email.com', 'hidden_fields': self.hidden_fields, + 'organisasi_komunitas': 'komunitas tanpa batas', + 'hidden_fields_verbose': self.hidden_fields_verbose, + 'can_see_hidden_fields': True, 'hidden_fields_color': '#808080', + } + + data = serializer.data + data.pop('foto') + + self.assertEqual(data, expected) + + def test_can_update_should_not_change_when_sending_empty_string_image(self): + user = User.objects.create_user(**self.mock_user) + bisagouser = BisaGoUser.objects.create(user=user, + **self.mock_bisagouser, foto=self.upload_image) + + update = { + 'name': 'another dummy name', 'phone_number': '99999999', + 'seen': False, 'alamat': 'dif dummy st', 'foto': MockEmptyImage('pic.jpg'), + 'jenis_kelamin': 'laki-laki', 'tanggal_lahir': '2000-10-02', + 'disabilitas': 'tanpa batas', 'pekerjaan': 'makan', + 'organisasi_komunitas': 'komunitas tanpa batas' + } + request = MockRequest(user_id=user.id) + + serializer = BisaGoUserSerializer(bisagouser, data=update, + context={'request':request}) + serializer.is_valid(raise_exception=True) + serializer.save() + + expected = { + 'phone_number': '99999999', 'tanggal_lahir': '02 October 2000', + 'jenis_kelamin': 'laki-laki', 'disabilitas': 'tanpa batas', + 'pekerjaan': 'makan', 'alamat': 'dif dummy st', 'seen': False, + 'username': 'dummy username', 'name': 'another dummy name', + 'email': 'test@email.com', 'hidden_fields': self.hidden_fields, + 'organisasi_komunitas': 'komunitas tanpa batas', + 'foto': MockRequest().build_absolute_uri(bisagouser.foto.url), + 'hidden_fields_verbose': self.hidden_fields_verbose, + 'can_see_hidden_fields': True, 'hidden_fields_color': '#808080', + } + + data = serializer.data + + self.assertDictEqual(data, expected) + + +class TestRegisterUserSerializer(BaseTestSerializer): + def test_meta_model(self): + self.assertEqual(RegisterUserSerializer.Meta.model, User) + + def test_meta_fields(self): + self.assertSequenceEqual(RegisterUserSerializer.Meta.fields, ( + 'name', 'email', 'password', 'tanggal_lahir', 'jenis_kelamin', + 'disabilitas', 'pekerjaan', 'alamat', 'phone_number', 'foto', + 'organisasi_komunitas', + )) + + def test_meta_user_fields_mapper(self): + self.assertEqual(RegisterUserSerializer.Meta.create_user_fields_mapper, { + 'username': 'email', + 'last_name': 'name', + }) + + def test_meta_bisago_fields_mapper(self): + self.assertEqual(RegisterUserSerializer.Meta.create_bisago_fields_mapper, {}) + + def test_validate_email_already_exists(self): + user = User.objects.create(username='dummy@test.com') + + serializer = RegisterUserSerializer() + + with self.assertRaises(serializers.ValidationError) as err: + serializer.validate_email(user.username) + + self.assertEqual(err.expected, serializers.ValidationError) + self.assertEqual('email already exists.', err.exception.args[0]) + + def test_validate_email_doesnt_exists(self): + User.objects.create(username='dummy@test.com') + + serializer = RegisterUserSerializer() + + email = serializer.validate_email('another@test.com') + + self.assertEqual(email, 'another@test.com') + + def test_validate_phone_number_doesnt_match_regex(self): + serializer = RegisterUserSerializer() + + with self.assertRaises(serializers.ValidationError) as err: + serializer.validate_phone_number('0000') + + self.assertEqual(err.expected, serializers.ValidationError) + self.assertEqual('invalid phone number.', err.exception.args[0]) + + def test_validate_phone_number_already_exists(self): + user = User.objects.create_user(username='dummy username') + bisagouser = BisaGoUser.objects.create(user=user, + phone_number='000088881111') + serializer = RegisterUserSerializer() + + with self.assertRaises(serializers.ValidationError) as err: + serializer.validate_phone_number(bisagouser.phone_number) + + self.assertEqual(err.expected, serializers.ValidationError) + self.assertEqual('phone number already exists.', err.exception.args[0]) + + def test_validate_phone_number_valid(self): + user = User.objects.create(username='dummy username') + BisaGoUser.objects.create(user=user, phone_number='000011112222') + + serializer = RegisterUserSerializer() + + phone_number = serializer.validate_phone_number('000011112223') + + self.assertEqual(phone_number, '000011112223') + + @patch('registrasi.serializers.send_activation_email') + def test_create_and_send_activation_email_is_called(self, mock_activation_email): + data = { + 'name': 'Dummy the boi', + 'email': 'dummy@test.com', + 'password': 'password', + 'phone_number': '000011112222', + 'tanggal_lahir': '2000-01-01', + 'jenis_kelamin': 'laki-laki', + 'foto': self.upload_image, + 'organisasi_komunitas': 'komunitas lain', + } + + request = MockRequest() + serializer = RegisterUserSerializer(data=data, context={'request':request}) + + serializer.is_valid(raise_exception=True) + serializer.save() + + user = User.objects.get(username='dummy@test.com') + mock_activation_email.assert_called_once_with(user, request) + + @patch('registrasi.serializers.print') + @patch('registrasi.serializers.send_activation_email', side_effect=ConnectionError()) + def test_create_and_send_activation_email_connection_error(self, + mock_activation_email, mock_print_connection_error): + data = { + 'name': 'Dummy the boi', + 'email': 'dummy@test.com', + 'password': 'password', + 'phone_number': '000011112222', + 'tanggal_lahir': '2000-01-01', + 'jenis_kelamin': 'laki-laki', + 'foto': self.upload_image, + 'organisasi_komunitas': 'komunitas lain', + } + + request = MockRequest() + serializer = RegisterUserSerializer(data=data, context={'request':request}) + + serializer.is_valid(raise_exception=True) + + serializer.save() + + mock_print_connection_error.assert_called_once_with( + 'Failed to send activation email to %s, connection error.' %\ + (data['email'])) diff --git a/registrasi/tests.py b/registrasi/tests.py index 32f7ca8dd903fbc02a2be41783dec507034a9409..62e1fb5932884b780aa178d2ed0dd1e40a818a33 100644 --- a/registrasi/tests.py +++ b/registrasi/tests.py @@ -3,9 +3,6 @@ from django.contrib.auth.models import User from django.db.utils import IntegrityError from .models import BisaGoUser - - - class RegistrationTest(TestCase): def test_user_model_created(self): user = User.objects.create(username="hoho", email="user@gmail.com", password="hohoho") @@ -17,4 +14,4 @@ class RegistrationTest(TestCase): with self.assertRaises(IntegrityError) as ex: obj = BisaGoUser(user=None) obj.save() - self.assertEqual(ex.expected, IntegrityError) + self.assertEqual(ex.expected, IntegrityError) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index eb157e87aeeafcf0d6cebb891ae4b1ad60d88198..e2767d609d8e1525217e499438318d62a582b38a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,11 @@ boto3==1.17.12 botocore==1.20.12 cachetools==4.2.1 certifi==2020.12.5 +cffi==1.14.5 chardet==4.0.0 +colorclass==2.2.0 coverage==5.4 +cryptography==3.4.6 defusedxml==0.6.0 dj-database-url==0.5.0 Django==3.1.7 @@ -19,9 +22,12 @@ django-storages==1.11.1 django-utils-six==2.0 djangorestframework==3.12.2 djangorestframework-simplejwt==4.6.0 +docopt==0.6.2 +google-api-core==1.26.1 google-api-python-client==1.12.8 google-auth==1.27.0 google-auth-httplib2==0.0.4 +googleapis-common-protos==1.53.0 gunicorn==20.0.4 httplib2==0.19.0 idna==2.6 @@ -30,7 +36,10 @@ jmespath==0.10.0 lazy-object-proxy==1.5.2 mccabe==0.6.1 oauthlib==3.1.0 +packaging==20.9 Pillow==8.1.0 +pip-upgrader==1.4.15 +protobuf==3.15.6 psycopg2-binary==2.8.6 pyasn1==0.4.8 pyasn1-modules==0.2.8 @@ -39,10 +48,10 @@ PyJWT==2.0.1 pylint==2.7.0 pylint-django==2.4.2 pylint-plugin-utils==0.6 +pyparsing==2.4.7 python-dateutil==2.8.1 python-dotenv==0.15.0 python3-openid==3.2.0 -pip-upgrader==1.4.15 pytz==2021.1 requests==2.25.1 requests-oauthlib==1.3.0 @@ -53,9 +62,16 @@ six==1.15.0 social-auth-app-django==4.0.0 social-auth-core==4.0.3 sqlparse==0.4.1 +terminaltables==3.1.0 toml==0.10.2 typed-ast==1.4.2 uritemplate==3.0.1 urllib3==1.26.3 whitenoise==5.2.0 wrapt==1.12.1 +fcm-django==0.3.10 +pyfcm==1.4.9 +django-mock-queries==2.1.6 +mock==4.0.3 +model-bakery==1.1.1 +colour==0.1.5 diff --git a/templates/acc_activate_success.html b/templates/acc_activate_success.html new file mode 100644 index 0000000000000000000000000000000000000000..8ecf1760685dfd120a0d723465aee0b396852939 --- /dev/null +++ b/templates/acc_activate_success.html @@ -0,0 +1,15 @@ +Aktivasi pengguna bisaGo +{% if success %} +Hai {{ user.last_name }}, +
+
+
+User anda berhasil teraktivasi, terima kasih telah menggunakan bisaGo. +
+
+Salam, +
+bisaGo dev Team +{% else %} +Terjadi kesalahan atau akun sudah teraktivasi, mohon hubungi tim pengembang bisaGo. +{% endif %} \ No newline at end of file diff --git a/templates/acc_active_email.html b/templates/acc_active_email.html index f900ca02d0df65849dbea677d95c71bcacec508f..6319940cdbd7926d6a1c428e2d60f58a78c446a1 100644 --- a/templates/acc_active_email.html +++ b/templates/acc_active_email.html @@ -1,8 +1,8 @@ {% autoescape off %} -Hai {{ user.username }}, +Hai {{ user.last_name }}, Selamat datang di aplikasi bisaGO. Sebelum anda bisa menggunakan akun anda, silahkan melakukan aktivasi dengan meng-klik link di bawah ini. -http://{{ domain }}{% url 'activate' uidb64=uid token=token %} +{{ absolute_uri }} Terima kasih dan selamat menggunakan bisaGO.