diff --git a/informasi_fasilitas/admin.py b/informasi_fasilitas/admin.py index 3f66350a7bb784d3026d24f14705cfdd35428893..da05be44a282fad589df572656387c4a8e6cae09 100644 --- a/informasi_fasilitas/admin.py +++ b/informasi_fasilitas/admin.py @@ -1,7 +1,36 @@ -from django.contrib import admin -from .models import Lokasi, Fasilitas, Komentar +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, Komentar, Kegiatan, KomentarKegiatan, FotoKegiatan + + +class KomentarAdmin(admin.ModelAdmin): + list_display = ('__str__', 'user', '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(Komentar, KomentarAdmin) +admin.site.register(Kegiatan) +admin.site.register(KomentarKegiatan, KomentarAdmin) +admin.site.register(FotoKegiatan) diff --git a/informasi_fasilitas/migrations/0013_fotokegiatan_kegiatan_komentarkegiatan.py b/informasi_fasilitas/migrations/0013_fotokegiatan_kegiatan_komentarkegiatan.py new file mode 100644 index 0000000000000000000000000000000000000000..b6e7a899e6eacb4dd936412feb991bd19c3694a4 --- /dev/null +++ b/informasi_fasilitas/migrations/0013_fotokegiatan_kegiatan_komentarkegiatan.py @@ -0,0 +1,48 @@ +# Generated by Django 3.1.7 on 2021-05-06 10:09 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('informasi_fasilitas', '0012_auto_20210329_1332'), + ] + + operations = [ + migrations.CreateModel( + name='Kegiatan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama_kegiatan', models.CharField(max_length=50)), + ('penyelenggara', models.CharField(max_length=50)), + ('deskripsi', models.TextField()), + ('time_start', models.DateTimeField(default=django.utils.timezone.now)), + ('time_end', models.DateTimeField(blank=True, null=True)), + ('lokasi', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='informasi_fasilitas.lokasi')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='KomentarKegiatan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deskripsi', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('kegiatan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='informasi_fasilitas.kegiatan')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='FotoKegiatan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('foto', models.ImageField(upload_to='')), + ('kegiatan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='informasi_fasilitas.kegiatan')), + ], + ), + ] diff --git a/informasi_fasilitas/migrations/0014_kegiatan_narahubung.py b/informasi_fasilitas/migrations/0014_kegiatan_narahubung.py new file mode 100644 index 0000000000000000000000000000000000000000..d94fea35da808cb8354e0fa310a4d5e3c3411e37 --- /dev/null +++ b/informasi_fasilitas/migrations/0014_kegiatan_narahubung.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-10 11:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0013_fotokegiatan_kegiatan_komentarkegiatan'), + ] + + operations = [ + migrations.AddField( + model_name='kegiatan', + name='narahubung', + field=models.TextField(null=True), + ), + ] diff --git a/informasi_fasilitas/migrations/0015_auto_20210511_0249.py b/informasi_fasilitas/migrations/0015_auto_20210511_0249.py new file mode 100644 index 0000000000000000000000000000000000000000..7aca39c748935ff7fd3b1b903313d19dad319ed1 --- /dev/null +++ b/informasi_fasilitas/migrations/0015_auto_20210511_0249.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.7 on 2021-05-11 02:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0014_kegiatan_narahubung'), + ] + + operations = [ + migrations.AlterField( + model_name='fotokegiatan', + name='foto', + field=models.ImageField(default=None, null=True, upload_to='kegiatan/'), + ), + migrations.AlterField( + model_name='fotokegiatan', + name='kegiatan', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='informasi_fasilitas.kegiatan'), + ), + ] diff --git a/informasi_fasilitas/migrations/0016_kegiatan_links.py b/informasi_fasilitas/migrations/0016_kegiatan_links.py new file mode 100644 index 0000000000000000000000000000000000000000..c49399f85137f4e1df7ad4327ed0d4b3b3b9ca37 --- /dev/null +++ b/informasi_fasilitas/migrations/0016_kegiatan_links.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-05-16 16:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('informasi_fasilitas', '0015_auto_20210511_0249'), + ] + + operations = [ + migrations.AddField( + model_name='kegiatan', + name='links', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/informasi_fasilitas/models.py b/informasi_fasilitas/models.py index 1e90a63677c1587c8225d9f9c48a8d5e9a46fa97..fe0c90d38b3f45efc08b890413cf52f378ccfaf6 100644 --- a/informasi_fasilitas/models.py +++ b/informasi_fasilitas/models.py @@ -73,6 +73,20 @@ class Fasilitas(models.Model): image = models.ImageField(upload_to="fasilitas/", null=True, default=None) is_verified = models.BooleanField(default=False) + +class Kegiatan(models.Model): + objects = models.Manager() + lokasi = models.ForeignKey(Lokasi, on_delete=models.CASCADE) + 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) + deskripsi = models.TextField(null=False) + links = models.TextField(null=True, blank=True) + 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) @@ -80,6 +94,9 @@ class Komentar(models.Model): date_time = models.DateTimeField(auto_now_add=True) deskripsi = models.TextField() + def __str__(self): + return self.deskripsi + class Likes(models.Model): objects = models.Manager() user = models.ForeignKey(User, on_delete=models.CASCADE) @@ -90,4 +107,19 @@ 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) \ No newline at end of file + 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) + + def __str__(self): + return self.deskripsi + +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) diff --git a/informasi_fasilitas/serializers.py b/informasi_fasilitas/serializers.py index e191b99cdb7bc7ddd15b8ec5b8b7fe1b2c1b91b2..20fc38a8913534bbe91f7581adefb5481651adc8 100644 --- a/informasi_fasilitas/serializers.py +++ b/informasi_fasilitas/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers -from .models import Lokasi +from .models import Lokasi, Kegiatan, KomentarKegiatan, FotoKegiatan + class LokasiSerializer(serializers.ModelSerializer): @@ -15,3 +16,48 @@ class LokasiSerializer(serializers.ModelSerializer): 'read_only': True, }, } + + +class FotoKegiatanSerializer(serializers.ModelSerializer): + place_id = serializers.CharField(source='lokasi.place_id', read_only=True) + class Meta: + model = FotoKegiatan + fields = '__all__' + extra_kwargs = { + 'foto': {'required': True}, + 'kegiatan': {'required': True}, + } + + +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', 'creator_email', + 'nama_kegiatan', 'penyelenggara', 'deskripsi', + "links","narahubung", 'time_start', 'time_end') + extra_kwargs = { + 'nama_kegiatan': {'required': True}, + 'penyelenggara': {'required': True}, + 'deskripsi': {'required': True}, + 'narahubung': {"required": True}, + 'time_start': {'required': True}, + } + + +class KomentarKegiatanSerializer(serializers.ModelSerializer): + kegiatan = serializers.IntegerField(source='kegiatan.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 = KomentarKegiatan + fields = ('id', 'creator', 'kegiatan', 'deskripsi', 'created', + 'creator_email') + 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..9bf6e1f748f7217873eafbbb502f69c15ce486b3 --- /dev/null +++ b/informasi_fasilitas/test_admin.py @@ -0,0 +1,85 @@ +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', '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 5db110f2aa0ed49194505648498ddb7395e9bf14..e481d3d379185b75e487e56f5651a6f595f38dbf 100644 --- a/informasi_fasilitas/test_base.py +++ b/informasi_fasilitas/test_base.py @@ -1,6 +1,7 @@ import json import tempfile +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 @@ -13,7 +14,10 @@ from .models import ( Komentar, KURSI_RODA, Likes, - Dislikes + Dislikes, + Kegiatan, + FotoKegiatan, + KomentarKegiatan ) from pplbackend.utils import get_client_login_with_user @@ -44,6 +48,25 @@ class InformasiFasilitasTest(TestCase): 'deskripsi': 'sangat membantu', } + # Waktu mungkin perlu disesuaikan lagi test dan implementasinya + 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_komentar_kegiatan_test = { + 'deskripsi': 'sangat membantu' + } + + mock_foto_kegiatan_test = { + 'foto' : '/media/lokasi/Screen_Shot_2020-12-29_at_01.48.32.png', + } + def create_user_test(self, user_dict=mock_user_test): return User.objects.create_user(**user_dict) @@ -229,6 +252,53 @@ class InformasiFasilitasTest(TestCase): user=user, fasilitas=fasilitas, ) + + def create_kegiatan_test( + self, + user=None, + user_dict=mock_user_test, + lokasi=None, + lokasi_dict=mock_lokasi_test, + kegiatan_dict=mock_kegiatan_test + ): + user = self.get_or_create_user_test( + user=user, + user_dict=user_dict, + ) + + lokasi = self.get_or_create_lokasi_test( + lokasi=lokasi, + lokasi_dict=lokasi_dict, + ) + return Kegiatan.objects.create(**kegiatan_dict, user=user, lokasi=lokasi) + + def create_foto_kegiatan_test( + self, + foto_kegiatan_dict=mock_foto_kegiatan_test + ): + kegiatan = self.create_kegiatan_test() + return FotoKegiatan.objects.create(**foto_kegiatan_dict, kegiatan=kegiatan) + + def create_komentar_kegiatan_test( + self, + user=None, + komentar_kegiatan_dict=mock_komentar_kegiatan_test, + user_dict=mock_user_test, + kegiatan=None + ): + user = self.get_or_create_user_test( + user=user, + user_dict=user_dict, + ) + + if kegiatan is None: + kegiatan = self.create_kegiatan_test() + + return KomentarKegiatan.objects.create( + **komentar_kegiatan_dict, + kegiatan=kegiatan, + user=user + ) class InformasiFasilitasViewTest(InformasiFasilitasTest): diff --git a/informasi_fasilitas/test_models.py b/informasi_fasilitas/test_models.py index b155d232fa498b698ba0ae461a138034b15a63f8..214c04e8bf022818e244c23f4b1c73c0c0a8ad28 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 +from .models import (Lokasi, Fasilitas, Komentar, Likes, Dislikes, + Kegiatan, FotoKegiatan, KomentarKegiatan) class InformasiFasilitasModelTest(InformasiFasilitasTest): @@ -57,6 +58,10 @@ class InformasiFasilitasModelTest(InformasiFasilitasTest): count = Komentar.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,3 +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_views_kegiatan.py b/informasi_fasilitas/test_views_kegiatan.py new file mode 100644 index 0000000000000000000000000000000000000000..d8545b17f405f3456c576d648546f0e811bf2335 --- /dev/null +++ b/informasi_fasilitas/test_views_kegiatan.py @@ -0,0 +1,250 @@ +from unittest.mock import patch +from datetime import timedelta +from http import HTTPStatus + +import json +import shutil +import os + +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 .test_base import InformasiFasilitasViewTest +from .models import Kegiatan, FotoKegiatan + +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.kwargs_place_id = {'place_id': self.lokasi.place_id} + self.kwargs_add_or_update_kegiatan = { + 'place_id': self.lokasi.place_id, + 'id': self.kegiatan.id, + } + self.kwargs_get_foto_kegiatan = { + 'place_id': self.lokasi.place_id, + 'kegiatan_id': self.kegiatan.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} + + image_path1, image_path2 = ("test_file/test1.jpg", "test_file/test2.jpg") + image1 = SimpleUploadedFile("test1.jpg", content=open(image_path1, 'rb').read(), + content_type='image/jpeg') + 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.get_list_kegiatan_url = \ + reverse('list-kegiatan', kwargs=self.kwargs_place_id) + 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) + 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) + + def tearDown(self): + try: + shutil.rmtree(self.media_root) + except OSError: + pass + + + @override_settings(MEDIA_ROOT=("test_file" + '/media')) + def test_can_add_kegiatan(self): + Kegiatan.objects.all().delete() + response_params = self.mock_kegiatan_test.copy() + response_params.update(self.kegiatan_images) + response = \ + self.client.post(self.add_kegiatan_url, response_params) + data = response.json() + data.pop("id", None) + expected_json = self.mock_kegiatan_test.copy() + expected_json.update({'creator': 'mock last_name', + 'place_id': 'mock_place_id', + 'creator_email': self.mock_user_test['email'] + }) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + self.assertDictEqual(data, expected_json) + count = Kegiatan.objects.all().count() + count_foto = FotoKegiatan.objects.all().count() + self.assertEqual(count, 1) + self.assertEqual(count_foto, 2) + + def test_add_kegiatan_with_missing_params(self): + Kegiatan.objects.all().delete() + response_params = self.mock_kegiatan_test.copy() + response_params.pop("time_start", None) + response = \ + self.client.post(self.add_kegiatan_url, response_params) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + + def test_add_kegiatan_lokasi_not_exist(self): + Kegiatan.objects.all().delete() + url = reverse('add-kegiatan', kwargs={ + 'place_id': "IDSNKCM", + }) + response_params = self.mock_kegiatan_test.copy() + response = self.client.post(url, response_params) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + def test_can_get_list_kegiatan(self): + response = Client().get(self.get_list_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, + 'narahubung': self.kegiatan.narahubung, + 'time_start': self.kegiatan.time_start, + 'time_end': self.kegiatan.time_end, + }, + } + self.assertEqual(content, expected_json) + + def test_can_get_detail_kegiatan(self): + response = Client().get(self.get_detail_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, + '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, + } + self.assertEqual(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, + '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, + } + self.assertEqual(content, expected_json) + + @patch('informasi_fasilitas.views_kegiatan.timezone') + def test_empty_get_nearest_kegiatan(self, mock_timezone): + mock_timezone.now.return_value = timezone.now() + timedelta(days=100) + response = Client().get(self.get_nearest_kegiatan_url) + self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND) + + @override_settings(MEDIA_ROOT=("test_file" + '/media')) + def test_can_put_update_kegiatan(self): + send_data = { + 'deskripsi': 'deskripsi kegiatan baru untuk test', + 'nama_kegiatan': 'nama kegiatan baru untuk test', + } + send_data.update(self.kegiatan_images) + response = self.client.put(self.update_kegiatan_url, data=send_data) + data = response.json() + data.pop("id", None) + expected_json = self.mock_kegiatan_test.copy() + expected_json.update({'creator': 'mock last_name', + 'place_id': 'mock_place_id', + 'creator_email': self.mock_user_test['email'] + }) + send_data.pop("images") + expected_json.update(send_data) + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) + self.assertDictEqual(data, expected_json) + + count = Kegiatan.objects.all().count() + count_foto = FotoKegiatan.objects.all().count() + 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={ + 'place_id': "IDSNKCM", + 'id': 200, + }) + send_data = { + 'deskripsi': 'deskripsi kegiatan baru untuk test', + 'nama_kegiatan': 'nama kegiatan baru untuk test', + 'narahubung': 'narahubung baru' + } + 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.assertEqual(item["kegiatan"], self.kegiatan.id) + counter += 1 + + 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, + 'narahubung': self.kegiatan.narahubung, + '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) diff --git a/informasi_fasilitas/test_views_komentar.py b/informasi_fasilitas/test_views_komentar.py index b81ba1f2ecd534bd28772d83ae874dbaf3694f67..d2b05eb4864fad943d27eb8b5a790b7e1ec56fe1 100644 --- a/informasi_fasilitas/test_views_komentar.py +++ b/informasi_fasilitas/test_views_komentar.py @@ -1,10 +1,9 @@ -import json from http import HTTPStatus from django.urls import reverse +from pplbackend.utils import response_decode from .test_base import InformasiFasilitasViewTest from .models import Komentar -from pplbackend.utils import response_decode from .views import TIME_FORMAT @@ -48,6 +47,7 @@ class KomentarRelatedViewTest(InformasiFasilitasViewTest): 'id': komentar.id, 'deskripsi': komentar.deskripsi, 'creator': komentar.user.last_name, + 'creator_email': komentar.user.email, 'date_time': komentar.date_time.strftime(TIME_FORMAT), } } diff --git a/informasi_fasilitas/test_views_komentar_kegiatan.py b/informasi_fasilitas/test_views_komentar_kegiatan.py new file mode 100644 index 0000000000000000000000000000000000000000..c712e2201833804497390f08361bcd7b65d3d518 --- /dev/null +++ b/informasi_fasilitas/test_views_komentar_kegiatan.py @@ -0,0 +1,111 @@ +import json +from http import HTTPStatus +from django.test import Client +from django.urls import reverse + +from .test_base import InformasiFasilitasViewTest +from .models import Lokasi, Kegiatan, KomentarKegiatan +from pplbackend.utils import response_decode + +class KomentarKegiatanRelatedViewTest(InformasiFasilitasViewTest): + + def setUp(self): + super().setUp() + self.kegiatan = self.create_kegiatan_test(self.user, self.lokasi) + self.kwargs_place_id = {'place_id': self.lokasi.place_id} + 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.list_komentar_kegiatan_url = \ + reverse('list-komentar-kegiatan', kwargs=self.kwargs_kegiatan_id) + + def test_can_add_komentar_kegiatan(self): + KomentarKegiatan.objects.all().delete() + response = \ + 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() + 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() + self.assertEqual(count, 0) + + def test_can_get_list_komentar_kegiatan(self): + komentar = self.create_komentar_kegiatan_test(user=self.user, kegiatan=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_deskripsi = komentar.deskripsi + + self.assertEqual(response_json[str(komentar.id)]['id'], expected_id) + self.assertEqual(response_json[str(komentar.id)]['creator'], expected_creator) + self.assertEqual(response_json[str(komentar.id)]['kegiatan'], expected_kegiatan) + self.assertEqual(response_json[str(komentar.id)]['deskripsi'], expected_deskripsi) + 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) + kwargs_get_komentar_kegiatan = { + 'place_id': self.lokasi.place_id, + 'kegiatan_id': self.kegiatan.id, + 'komentar_id': komentar.id, + } + get_komentar_kegiatan_url = \ + reverse('get-komentar-kegiatan', kwargs=kwargs_get_komentar_kegiatan) + response = self.client.get(get_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_deskripsi = komentar.deskripsi + + self.assertEqual(response_json['id'], expected_id) + self.assertEqual(response_json['creator'], expected_creator) + 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) + + kwargs_delete_komentar_kegiatan = { + 'place_id': self.lokasi.place_id, + 'kegiatan_id': self.kegiatan.id, + 'komentar_id': komentar.id, + } + + 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() + 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 eacb246c2f4ce743d31caa4688f669a44ba99daa..04a9c4f8337131bc426557f259ca8c24cf33492b 100644 --- a/informasi_fasilitas/urls.py +++ b/informasi_fasilitas/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from . import views +from . import views, views_kegiatan, views_komentar_kegiatan list_lokasi_views = views.LokasiListCreateView.as_view({'get':'list'}) add_lokasi_views = views.LokasiListCreateView.as_view({'post':'create'}) @@ -29,4 +29,39 @@ urlpatterns = [ path('lokasi/like-fasilitas////', views.update_like_fasilitas, name='update-like-fasilitas'), + + path('lokasi/list-kegiatan//', + views_kegiatan.list_kegiatan, name='list-kegiatan'), + + path('lokasi/detail-kegiatan///', + views_kegiatan.detail_kegiatan, name='detail-kegiatan'), + + 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'), + + path('lokasi/update-kegiatan///', + views_kegiatan.update_kegiatan, name='update-kegiatan'), + + path('lokasi/list-foto-kegiatan//', + views_kegiatan.list_foto_kegiatan, name='list-foto-kegiatan'), + + path('lokasi/get-komentar-kegiatan///', + views_komentar_kegiatan.get_komentar_kegiatan, name='get-komentar-kegiatan'), + + path('lokasi/list-komentar-kegiatan//', + views_komentar_kegiatan.list_komentar_kegiatan, name='list-komentar-kegiatan'), + + 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..6c49955ebe1adfc7f4e9ff79e1a3129b9c41b06f 100644 --- a/informasi_fasilitas/views.py +++ b/informasi_fasilitas/views.py @@ -1,18 +1,12 @@ 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 LokasiSerializer from .models import Lokasi, Fasilitas, Komentar, Likes, Dislikes @@ -251,6 +245,7 @@ def add_komentar(request, place_id, id): komentar = Komentar.objects.create(fasilitas=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) @@ -275,6 +270,7 @@ 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["creator_email"] = komentar.user.email komentar_details["date_time"] = komentar.date_time.strftime( TIME_FORMAT) return JsonResponse(return_json, status=HTTPStatus.OK) diff --git a/informasi_fasilitas/views_kegiatan.py b/informasi_fasilitas/views_kegiatan.py new file mode 100644 index 0000000000000000000000000000000000000000..3228f1a71e6674b9e8d60c626f67050d0aa3bfc5 --- /dev/null +++ b/informasi_fasilitas/views_kegiatan.py @@ -0,0 +1,129 @@ +from http import HTTPStatus +from django.http import JsonResponse +from django.utils import timezone +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.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 + + +@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) + 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 detail_kegiatan(request, place_id, id): + try: + queryset = Kegiatan.objects.get(lokasi__place_id=place_id, id=id) + serializer = KegiatanSerializer(queryset, many=False) + return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.OK) + except Kegiatan.DoesNotExist: + raise NotFound(detail="Kegiatan doesn't exist") + + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +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) + return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.OK) + +@api_view(['POST']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def add_kegiatan(request, place_id): + try: + lokasi = Lokasi.objects.get(place_id=place_id) + user = request.user + data = request.data.dict() + data.update({"user": user.id, "lokasi": lokasi.id}) + 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")) + + return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.CREATED) + except Lokasi.DoesNotExist: + raise NotFound(detail="Lokasi doesn't exist") + + +@api_view(['PUT']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +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", []) + 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")) + + return JsonResponse(_clean_json_kegiatan(serializer.data), status=HTTPStatus.ACCEPTED) + except Kegiatan.DoesNotExist: + raise NotFound(detail="Kegiatan doesn't exist") + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def list_foto_kegiatan(request, place_id, kegiatan_id): + queryset = FotoKegiatan.objects.filter(kegiatan__lokasi__place_id=place_id, + kegiatan_id=kegiatan_id) + serializer = FotoKegiatanSerializer(queryset, many=True) + data_response = serializer.data + new_dict = {item['id']: dict(item) for item in data_response} + return JsonResponse(new_dict, status=HTTPStatus.OK) + +@api_view(['GET']) +@authentication_classes([]) +@permission_classes([]) +def search_kegiatan(request, query): + 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) + queryset = query_by_nama | query_by_deskripsi | query_by_penyelenggara + if not queryset.exists(): + raise NotFound(detail="No Kegiatan available") + serializer = KegiatanSerializer(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 + + +def _add_foto_kegiatan(kegiatan, list_image): + _create_list_kegiatan_foto(kegiatan, list_image) + kegiatan.refresh_from_db() + return KegiatanSerializer(kegiatan) + + +def _create_list_kegiatan_foto(kegiatan, list_image): + list_kegiatan_foto = [] + for image in list_image: + foto = FotoKegiatan.objects.create(kegiatan=kegiatan, foto=image) + list_kegiatan_foto.append(foto) + return list_kegiatan_foto diff --git a/informasi_fasilitas/views_komentar_kegiatan.py b/informasi_fasilitas/views_komentar_kegiatan.py new file mode 100644 index 0000000000000000000000000000000000000000..af6718d83eb60513787829db3a44a336fd34e500 --- /dev/null +++ b/informasi_fasilitas/views_komentar_kegiatan.py @@ -0,0 +1,67 @@ +from http import HTTPStatus +from django.http import JsonResponse +from django.core.exceptions import ObjectDoesNotExist + +from rest_framework.decorators import api_view, permission_classes, authentication_classes +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from notification.utils import send_komentar_notification + +from .serializers import KomentarKegiatanSerializer +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) + serializer = KomentarKegiatanSerializer(queryset, 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) + +@api_view(['GET']) +@authentication_classes([]) +@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) + serializer = KomentarKegiatanSerializer(komentar, many=False) + return JsonResponse(serializer.data, status=HTTPStatus.OK) + except ObjectDoesNotExist: + return JsonResponse({'response': 'Komentar tidak ditemukan'}, status=HTTPStatus.NOT_FOUND) + +@api_view(['POST']) +@authentication_classes([TokenAuthentication]) +@permission_classes([IsAuthenticated]) +def add_komentar_kegiatan(request, place_id, kegiatan_id): + try: + kegiatan = Kegiatan.objects.get(lokasi__place_id=place_id, id=kegiatan_id) + deskripsi = request.POST['deskripsi'] + komentar = KomentarKegiatan.objects \ + .create( + kegiatan=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: + return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) + +@api_view(['DELETE']) +@authentication_classes([TokenAuthentication]) +@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.delete() + return JsonResponse({'response': 'komentar kegiatan deleted', 'id': komentar_id}, + status=HTTPStatus.OK) + except Exception as error: + return JsonResponse({'response': str(error)}, status=HTTPStatus.NOT_FOUND) 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..8666c40e1d43a909866a28d8b963387662082398 --- /dev/null +++ b/new_rest_api/permissions.py @@ -0,0 +1,14 @@ +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 == 'update': + return request.user.is_authenticated + return False + + def has_object_permission(self, request, view, obj): + if view.action == 'update': + return request.user.id == obj.user.id + return True diff --git a/new_rest_api/test_permissions.py b/new_rest_api/test_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..6569caa6ef48eff33727a27b565cf4c5417566d7 --- /dev/null +++ b/new_rest_api/test_permissions.py @@ -0,0 +1,103 @@ +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') + + # 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) + + # 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_other_action(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.assertTrue(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..4fa9308029a79fcdfaa05ce60f9dd6c3e035813f --- /dev/null +++ b/new_rest_api/test_views.py @@ -0,0 +1,153 @@ +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 + + +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, + } + + @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) + + @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 = {'response': 'User activated'} + + 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 = {'response': 'error'} + + user.refresh_from_db() + self.assertFalse(user.is_active) + self.assertEqual(resp.status_code, status.BAD_REQUEST) + 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 = {'response': 'error'} + + user.refresh_from_db() + self.assertFalse(user.is_active) + self.assertEqual(resp.status_code, status.BAD_REQUEST) + 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..4e92703e616dca2b4fbe33ca5fd9ffe6ad1a5ac3 100644 --- a/new_rest_api/views.py +++ b/new_rest_api/views.py @@ -1,165 +1,56 @@ 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.http import JsonResponse +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.authentication import TokenAuthentication +from rest_framework.decorators import action +from rest_framework.response import Response from registrasi.models import BisaGoUser -from registrasi.serializers import BisaGoUserSerializers +from registrasi.serializers import BisaGoUserSerializers, RegisterUserSerializer from .tokens import account_activation_token +from .permissions import UserViewPermission -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) +ACTIVATION_URL_REGEX =\ + r'activate/(?P[0-9A-Za-z_-]+)/(?P[0-9A-Za-z]+-[0-9A-Za-z]{32})' -@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) +class UserViewSet(viewsets.ModelViewSet): + queryset = BisaGoUser.objects.all() + serializer_class = BisaGoUserSerializers + lookup_field = 'user__username' + lookup_value_regex = '[^/]+' + authentication_classes = (TokenAuthentication,) + permission_classes = (UserViewPermission,) -@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:]) + @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) - 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) + @action(detail=False, methods=['GET'], url_path=ACTIVATION_URL_REGEX) + def activate(self, request, uidb64, token): + return activate(uidb64, token) -@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): +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 JsonResponse({'response': 'error'}, status=status.BAD_REQUEST) + if account_activation_token.check_token(user, token): + user.is_active = True + user.save() + return JsonResponse({'response': 'User activated'}, status=status.OK) + return JsonResponse({'response': 'error'}, status=status.BAD_REQUEST) 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/permissions.py b/notification/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..566e4731731ad0f3ef65f3dae768b076b329ce96 --- /dev/null +++ b/notification/permissions.py @@ -0,0 +1,7 @@ +from rest_framework import permissions + +class NotificationPermission(permissions.BasePermission): + def has_permission(self, request, view): + if view.action in ['list', 'create']: + return request.user.is_authenticated + return False 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_permissions.py b/notification/test_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..29ff48d4dea3f7aeada91fad2cc1c1c735d685a7 --- /dev/null +++ b/notification/test_permissions.py @@ -0,0 +1,67 @@ +from django.test import TestCase +from .permissions import NotificationPermission + +class MockUserAuthenticated: + is_authenticated = True + +class MockUserNotAuthenticated: + is_authenticated = False + +class MockRequest: + def __init__(self, user): + self.user = user + +class MockView: + def __init__(self, action): + self.action = action + +class TestPermission(TestCase): + def setUp(self): + self.notification_permission = NotificationPermission() + self.authenticated_request = MockRequest(MockUserAuthenticated()) + self.not_authenticated_request = MockRequest(MockUserNotAuthenticated()) + + self.view_list = MockView('list') + self.view_create = MockView('create') + self.view_update = MockView('update') + self.view_delete = MockView('delete') + + def test_authenticated_can_list(self): + ret = self.notification_permission\ + .has_permission(self.authenticated_request, self.view_list) + self.assertTrue(ret) + + def test_not_authenticated_cant_list(self): + ret = self.notification_permission\ + .has_permission(self.not_authenticated_request, self.view_list) + self.assertFalse(ret) + + def test_authenticated_can_create(self): + ret = self.notification_permission\ + .has_permission(self.authenticated_request, self.view_create) + self.assertTrue(ret) + + def test_not_authenticated_cant_create(self): + ret = self.notification_permission\ + .has_permission(self.not_authenticated_request, self.view_create) + self.assertFalse(ret) + + def test_authenticated_cant_update(self): + ret = self.notification_permission\ + .has_permission(self.authenticated_request, self.view_update) + self.assertFalse(ret) + + def test_not_authenticated_cant_update(self): + ret = self.notification_permission\ + .has_permission(self.not_authenticated_request, self.view_update) + self.assertFalse(ret) + + def test_authenticated_cant_delete(self): + ret = self.notification_permission\ + .has_permission(self.authenticated_request, self.view_delete) + self.assertFalse(ret) + + def test_not_authenticated_cant_delete(self): + ret = self.notification_permission\ + .has_permission(self.not_authenticated_request, self.view_delete) + self.assertFalse(ret) diff --git a/notification/test_utils.py b/notification/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..151857ce1d9758596dc14094e7c6badaaef7dd59 --- /dev/null +++ b/notification/test_utils.py @@ -0,0 +1,204 @@ +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 (Komentar, 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 = Komentar() + 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 = Komentar(fasilitas=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(kegiatan=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 = Komentar(fasilitas=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(kegiatan=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 = Komentar(fasilitas=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(kegiatan=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 = Komentar(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 = Komentar(fasilitas=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 = Komentar(fasilitas=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 = Komentar(fasilitas=fasilitas, user=user) + + ret_fcm_device = utils.get_target_fcm_device(komentar) + + self.assertEqual(len(ret_fcm_device), 0) + + def test_get_formatted_username_length_less_than_max_length_name_should_not_get_trimmed(self): + username = 'ariq' + ret_username = utils.get_formatted_user_name(username, max_length_name=10) + self.assertEqual(username, ret_username) + + def test_get_formatted_username_length_equal_max_length_name_should_not_get_trimmed(self): + username = 'ariqbasyar' + ret_username = utils.get_formatted_user_name(username, max_length_name=10) + self.assertEqual(username, ret_username) + + def test_get_formatted_username_length_more_than_max_length_name_should_get_trimmed(self): + username = 'ariqbasyarr' + + max_length_name = 10 + replacement_str = '...' + length_replacement = len(replacement_str) + ret_username = utils.get_formatted_user_name(username, max_length_name=max_length_name, + replacement_str=replacement_str) + + self.assertEqual(ret_username, '%s%s' % (username[:max_length_name - length_replacement], + replacement_str)) + self.assertEqual(ret_username, 'ariqbas...') + + def test_get_formatted_message_username_should_not_get_trimmed(self): + username = 'ariqbasyar' + ret_formatted_message = utils.get_formatted_message(username) + self.assertEqual(ret_formatted_message, 'ariqbasyar menambahkan komentar baru') + + def test_get_formatted_message_username_should_get_trimmed(self): + username = 'ariqbasyarr' + ret_formatted_message = utils.get_formatted_message(username) + self.assertEqual(ret_formatted_message, 'ariqbas... menambahkan komentar baru') + + @patch('notification.utils.get_target_fcm_device') + @patch('notification.utils.get_formatted_message') + @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 = Komentar(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_message = 'ariqbasyar menambahkan komentar baru' + + 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_message + args[5].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) + fcmdevice_mock.send_message.assert_called_once_with( + title=formatted_message, + body='"%s"' % komentar.deskripsi, + data={ + 'place_id': 'dummy place_id', + 'id': 1, + 'type': 'fasilitas', + 'message': formatted_message, + } + ) diff --git a/notification/test_views.py b/notification/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..14e6afad0fcf71eb2d158ed87ffe08981bfb54b8 --- /dev/null +++ b/notification/test_views.py @@ -0,0 +1,203 @@ +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, AbstractFCMDevice +from fcm_django.api.rest_framework import FCMDeviceSerializer + +from informasi_fasilitas.models import Lokasi, Kegiatan, Fasilitas, Komentar, 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.OK) + self.assertEqual(data, expected) + + def test_can_register_device_type_web_or_android_or_ios(self): + FCMDevice.objects.all().delete() + + device_types = list(map(lambda x: x[0], AbstractFCMDevice.DEVICE_TYPES)) + responses = [] + for _type in device_types: + resp = self.client.post(self.register_notification_url, data={ + 'token': 'simple token', + 'type': _type, + }) + responses.append(resp.status_code) + + self.assertEqual(responses, [HTTPStatus.OK] * len(device_types)) + + 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_return_existing_device_if_duplicate(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', + }) + + obj = FCMDevice.objects.filter(registration_id='a', active=True).first() + serializer = FCMDeviceSerializer(obj) + + expected = serializer.data + data = response_decode(resp) + + self.assertEqual(resp.status_code, HTTPStatus.OK) + 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) + + @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 = Komentar.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..1d7e50353074ab16231c805a6b3237b648469426 --- /dev/null +++ b/notification/utils.py @@ -0,0 +1,55 @@ +from fcm_django.models import FCMDevice + +from informasi_fasilitas.models import Komentar + +def get_type_informasi_from_komentar(komentar): + if isinstance(komentar, Komentar): + return 'fasilitas' + return 'kegiatan' + +def get_informasi(komentar): + if isinstance(komentar, Komentar): + return komentar.fasilitas + return komentar.kegiatan + +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_user_name(name, max_length_name=10, replacement_str='...'): + length_replacement = len(replacement_str) + return name if len(name) <= max_length_name else '%s%s' % ( + name[:max_length_name - length_replacement], replacement_str + ) + +def get_formatted_message(sender, **kwargs): + return '%s menambahkan komentar baru' % get_formatted_user_name(sender, **kwargs) + +def send_komentar_notification(komentar): + 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_message = get_formatted_message(sender) + target_fcm_device = get_target_fcm_device(komentar) + + return target_fcm_device.send_message( + title=formatted_message, + body='"%s"' % komentar.deskripsi, + data={ + 'place_id': place_id, + 'id': informasi_id, + 'type': informasi_type, + 'message': formatted_message, + } + ) diff --git a/notification/views.py b/notification/views.py new file mode 100644 index 0000000000000000000000000000000000000000..34fe60ad8199ce368d9f845c5cef7135dddf1882 --- /dev/null +++ b/notification/views.py @@ -0,0 +1,39 @@ +from http import HTTPStatus + +from rest_framework.authentication import TokenAuthentication +from rest_framework.response import Response +from rest_framework import viewsets +from fcm_django.api.rest_framework import FCMDeviceViewSet + +from .permissions import NotificationPermission + + +class BaseNotificationViews(viewsets.ModelViewSet): + authentication_classes = [TokenAuthentication] + +class NotificationViews(BaseNotificationViews, FCMDeviceViewSet): + permission_classes = (NotificationPermission,) + + def get_queryset(self): + return self.queryset.filter(user=self.request.user) + + def create(self, request): + data = request.data + fcm_token = data.get('token') + device_type = data.get('type') + existing_fcm_device = self.get_queryset().filter(registration_id=fcm_token) + if existing_fcm_device.exists(): + serializer = self.get_serializer(existing_fcm_device.first()) + return Response(serializer.data, status=HTTPStatus.OK) + serializer = self.get_serializer(data={ + 'name': request.user.last_name, + 'active': request.user.is_active, + 'device_id': '%s - %s' % (request.user.last_name, + fcm_token[:48] if fcm_token else ''), + 'registration_id': fcm_token, + 'type': device_type, + }) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=HTTPStatus.OK) + return Response(serializer.errors, status=HTTPStatus.BAD_REQUEST) diff --git a/oauth/views.py b/oauth/views.py index d516f8650c3d7f446fb9504d1da79fb2a0539601..43e1267617e9b7821b05f977cb54d29a70829398 100644 --- a/oauth/views.py +++ b/oauth/views.py @@ -13,8 +13,6 @@ from django.contrib.auth.models import User from django.conf import settings from registrasi.models import BisaGoUser - - @csrf_exempt @api_view(http_method_names=['POST']) @permission_classes([]) @@ -32,7 +30,6 @@ def request_token(request): user = result return _check_user(user) - def _google_check(access_token, name): try: result = validate_google_token(access_token) @@ -44,7 +41,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,7 +51,6 @@ def _check_normal_auth(request, email, password): raise NotFound(detail="User doesn't exist") return user - def _check_user(user): if user.is_active: token, create = Token.objects.get_or_create(user=user) @@ -64,7 +59,6 @@ def _check_user(user): else: raise AuthenticationFailed(detail="Please activate your account") - def _create_google_user(email, name): try: return User.objects.get(username=email) @@ -82,7 +76,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 +84,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 +91,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/settings.py b/pplbackend/settings.py index 0c7eeda773f1cb7ad05b4656f12c3e3f061fdf95..cb98bad88939aef734d6d2d7c6c27d66cbeae7ca 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,6 +202,7 @@ SOCIALACCOUNT_PROVIDERS = { LOGIN_REDIRECT_URL = '/' REST_FRAMEWORK = { + 'DATETIME_FORMAT': "%Y-%m-%d %H:%M", 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, 'DEFAULT_AUTHENTICATION_CLASSES': [ @@ -242,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 dac4c0a3b39418e85be45afac803d34fb7e2399f..1273caff9f5d28206030d7b79bef99432c260406 100644 --- a/pplbackend/utils.py +++ b/pplbackend/utils.py @@ -1,7 +1,7 @@ from rest_framework.test import APIClient from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.authtoken.models import Token - +from rest_framework.test import APIClient from django.urls import reverse from django.test import Client @@ -9,7 +9,7 @@ import json def get_client_login_with_user(user): token, _ = Token.objects.get_or_create(user=user) - client = Client(HTTP_AUTHORIZATION=f'token {token.key}') + client = APIClient(HTTP_AUTHORIZATION=f'token {token.key}') return client diff --git a/registrasi/migrations/0005_auto_20210516_1734.py b/registrasi/migrations/0005_auto_20210516_1734.py new file mode 100644 index 0000000000000000000000000000000000000000..a627fce94b9cf352ffa8b50c5d3a9341f7ebe86b --- /dev/null +++ b/registrasi/migrations/0005_auto_20210516_1734.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.7 on 2021-05-16 17:34 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0004_auto_20201210_1459'), + ] + + operations = [ + migrations.AlterField( + model_name='bisagouser', + name='tanggal_lahir', + field=models.DateField(default=datetime.date.today, max_length=15, verbose_name='Tanggal Lahir'), + ), + ] diff --git a/registrasi/migrations/0006_merge_20210516_1820.py b/registrasi/migrations/0006_merge_20210516_1820.py new file mode 100644 index 0000000000000000000000000000000000000000..d6db4439e1fec3e1edc3a7be92cea376ba6977d4 --- /dev/null +++ b/registrasi/migrations/0006_merge_20210516_1820.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-16 18:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0005_bisagouser_foto'), + ('registrasi', '0005_auto_20210516_1734'), + ] + + operations = [ + ] diff --git a/registrasi/migrations/0007_merge_20210517_0447.py b/registrasi/migrations/0007_merge_20210517_0447.py new file mode 100644 index 0000000000000000000000000000000000000000..5760767c1a7e1e658dbf2cfe99ad5d456dcffb75 --- /dev/null +++ b/registrasi/migrations/0007_merge_20210517_0447.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.7 on 2021-05-17 04:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('registrasi', '0006_bisagouser_seen'), + ('registrasi', '0006_merge_20210516_1820'), + ] + + 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/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/models.py b/registrasi/models.py index cadf158aca62b2230dfff03da3472cc4e459363e..d96d0466d4123fed0f621181787e15543ce01aae 100644 --- a/registrasi/models.py +++ b/registrasi/models.py @@ -1,19 +1,23 @@ +from datetime import date + from django.db import models -from django.core.mail import send_mail from django.contrib.auth.models import User class BisaGoUser(models.Model): - class Meta: - db_table = "BisaGoUser" user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="phone_number") phone_number = models.CharField('Nomor Telepon', max_length=15, unique=True) - tanggal_lahir = models.DateField('Tanggal Lahir', max_length=15, null=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') + organisasi_komunitas = models.CharField('Organisasi / Komunitas', max_length=64, null=True, default='-') 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) + foto = models.ImageField('Foto', blank=True, default=None, null=True) 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..8690a55624bc5ce3b45ca6cccbcae009f908a5a8 100644 --- a/registrasi/serializers.py +++ b/registrasi/serializers.py @@ -1,21 +1,124 @@ from rest_framework import serializers -from django.db import models + +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): + 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) + 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', ) - extra_kwargs = { - 'foto': {'required': False}, - } \ No newline at end of file + hidden_fields = ('phone_number', 'email', 'alamat', 'tanggal_lahir') + hidden_replacement_char = '-' + update_fields_mapper = { + 'phone_number': 'phone_number', 'seen': 'seen', 'alamat': 'alamat', + 'jenis_kelamin': 'jenis_kelamin', 'tanggal_lahir': 'tanggal_lahir', + 'disabilitas': 'disabilitas', 'pekerjaan': 'pekerjaan', + 'foto': 'foto', 'organisasi_komunitas': 'organisasi_komunitas' + } + update_user_fields_mapper = { + 'last_name': 'last_name', + } + + 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: + representation[hidden_field] = self.Meta.hidden_replacement_char + return representation + + def update(self, instance, validated_data): + for key, value in self.Meta.update_fields_mapper.items(): + setattr(instance, key, validated_data.get(value)) + instance.save() + + user = instance.user + for key, value in self.Meta.update_user_fields_mapper.items(): + setattr(user, key, validated_data['user'].get(value)) + 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', + ) + user_fields_mapper = { + 'username': 'email', 'email': 'email', 'password': 'password', + 'last_name': 'name', + } + bisago_fields_mapper = { + 'phone_number': 'phone_number', 'tanggal_lahir': 'tanggal_lahir', + 'jenis_kelamin': 'jenis_kelamin', 'disabilitas': 'disabilitas', + 'pekerjaan': 'pekerjaan', 'alamat': 'alamat', 'foto': 'foto', + 'user': 'user', 'organisasi_komunitas': 'organisasi_komunitas', + } + + 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(self, validated_data): + user = User.objects.create_user( + **{key:validated_data.get(value) for key, value in\ + self.Meta.user_fields_mapper.items()\ + if validated_data.get(value)}, + is_active=False) + + validated_data['user'] = user + BisaGoUser.objects.create( + **{key:validated_data.get(value) for key, value in\ + self.Meta.bisago_fields_mapper.items()\ + if validated_data.get(value)}) + + 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..d1dcf313e8bfc4c16bb8d5c5eb5f01e80cd348f8 --- /dev/null +++ b/registrasi/test_serializers.py @@ -0,0 +1,298 @@ +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 BisaGoUserSerializers, 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 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 = SimpleUploadedFile("test1.jpg", + content=open("test_file/test1.jpg", 'rb').read(), content_type='image/jpeg') + +class TestBisaGoUserSerializer(BaseTestSerializer): + def test_meta_model(self): + self.assertEqual(BisaGoUserSerializers.Meta.model, BisaGoUser) + + def test_meta_fields(self): + self.assertSequenceEqual(BisaGoUserSerializers.Meta.fields, ( + 'phone_number', 'tanggal_lahir', 'jenis_kelamin', 'name', 'email', + 'disabilitas', 'pekerjaan', 'alamat', 'foto', 'seen', 'username', + 'organisasi_komunitas', + )) + + def test_meta_hidden_fields(self): + self.assertEqual(BisaGoUserSerializers.Meta.hidden_fields, + ('phone_number', 'email', 'alamat', 'tanggal_lahir')) + + def test_meta_hidden_replacement_char(self): + self.assertEqual(BisaGoUserSerializers.Meta.hidden_replacement_char, '-') + + def test_meta_update_fields_mapper(self): + self.assertDictEqual(BisaGoUserSerializers.Meta.update_fields_mapper, { + 'phone_number': 'phone_number', 'seen': 'seen', 'alamat': 'alamat', + 'jenis_kelamin': 'jenis_kelamin', 'tanggal_lahir': 'tanggal_lahir', + 'disabilitas': 'disabilitas', 'pekerjaan': 'pekerjaan', + 'foto': 'foto', 'organisasi_komunitas': 'organisasi_komunitas' + }) + + def test_meta_update_user_fields_mapper(self): + self.assertEqual(BisaGoUserSerializers.Meta.update_user_fields_mapper, { + 'last_name': 'last_name', + }) + + 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 = BisaGoUserSerializers(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': '-' + } + + 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 = BisaGoUserSerializers(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': '-', + } + + 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 = BisaGoUserSerializers(bisagouser, + context={'request':request}) + + replaced = BisaGoUserSerializers.Meta.hidden_replacement_char + expected = { + 'phone_number': replaced, 'tanggal_lahir': replaced, + 'jenis_kelamin': '-', 'disabilitas': 'Belum memilih', + 'pekerjaan': '-', 'alamat': '-', 'foto': None, 'seen': False, + 'username': 'dummy username', 'name': 'dummy lastname', + 'email': replaced, 'alamat': replaced, + 'organisasi_komunitas': '-' + } + + self.assertEqual(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.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 = BisaGoUserSerializers(bisagouser, data=update, + context={'request':request}) + serializer.is_valid(raise_exception=True) + serializer.save() + + expected = { + 'phone_number': '99999999', 'tanggal_lahir': '2000-10-02', + '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', + 'organisasi_komunitas': 'komunitas tanpa batas' + } + + data = serializer.data + data.pop('foto') + + self.assertEqual(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.user_fields_mapper, { + 'username': 'email', + 'email': 'email', + 'password': 'password', + 'last_name': 'name', + }) + + def test_meta_bisago_fields_mapper(self): + self.assertEqual(RegisterUserSerializer.Meta.bisago_fields_mapper, { + 'tanggal_lahir': 'tanggal_lahir', + 'jenis_kelamin': 'jenis_kelamin', + 'phone_number': 'phone_number', + 'disabilitas': 'disabilitas', + 'organisasi_komunitas': 'organisasi_komunitas', + 'pekerjaan': 'pekerjaan', + 'alamat': 'alamat', + 'foto': 'foto', + 'user': 'user', + }) + + 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.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.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..589b95d5622ed58152340455bed94dbce759f39f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,3 +59,8 @@ 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 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. diff --git a/test_file/test1.jpg b/test_file/test1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5521673831b4e2d02eed407942bbeda242fd6e2d Binary files /dev/null and b/test_file/test1.jpg differ diff --git a/test_file/test2.jpg b/test_file/test2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2247108904eaf243c6e0b77d58611aae4c0c8878 Binary files /dev/null and b/test_file/test2.jpg differ