From 1fe323cbdd12312628d6d53ef7df7e6d7de20fe7 Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Sat, 7 Nov 2020 20:18:59 +0200 Subject: [PATCH 1/6] Add soft delete API for signups --- webapp/migrations/0074_signup_deleted.py | 18 ++++++++++++++++++ webapp/models.py | 4 ++-- webapp/tests/test_signup.py | 9 +++++++++ webapp/views.py | 11 ++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 webapp/migrations/0074_signup_deleted.py diff --git a/webapp/migrations/0074_signup_deleted.py b/webapp/migrations/0074_signup_deleted.py new file mode 100644 index 0000000..2526461 --- /dev/null +++ b/webapp/migrations/0074_signup_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2020-11-07 18:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0073_auto_20201107_1916'), + ] + + operations = [ + migrations.AddField( + model_name='signup', + name='deleted', + field=models.BooleanField(default=False), + ), + ] diff --git a/webapp/models.py b/webapp/models.py index 6137b23..3349e25 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -125,7 +125,7 @@ class SignupForm(models.Model): class Signup(models.Model): """ - In + Actual signup into any SignupForm. Deletes are soft. """ class Meta: @@ -140,6 +140,7 @@ class Signup(models.Model): email = models.EmailField(blank=True, null=True) # Random unique identifier. Used for signup editing by the user. uuid = models.UUIDField(default=uuid4, editable=False) + deleted = models.BooleanField(default=False) def __str__(self): return f"{self.signupForm}: {self.list_name} ({self.pk})" @@ -147,7 +148,6 @@ class Signup(models.Model): @receiver(post_save, sender=Signup) def email_on_signup(sender, instance, created, **kwargs): - """Send email validation.""" if created and instance.email: # TODO: Possible bug due to many-to-many relationship with events and forms. # TODO: Subject field crashes with lazy loaded translations. diff --git a/webapp/tests/test_signup.py b/webapp/tests/test_signup.py index 4ec1ef8..5f7f421 100644 --- a/webapp/tests/test_signup.py +++ b/webapp/tests/test_signup.py @@ -56,6 +56,15 @@ class SignupTestCase(APITestCase): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Signup.objects.count(), 3) + def test_delete_as_admin(self): + id = self.signup1.id + no_auth_response = self.client.delete(f"{URL}{id}/", format="json") + self.assertEqual(no_auth_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.client.force_authenticate(user=self.authClient) + response = self.client.delete(f"{URL}{id}/", format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Signup.objects.get(id=id).deleted, True) + @skip("NotImplemented") def test_get_hidden_forms_admin(self): pass diff --git a/webapp/views.py b/webapp/views.py index 87f2d0d..3800daa 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -106,7 +106,7 @@ class SignupFormViewSet(ModelViewSet): class SignupViewSet(ModelViewSet): - queryset = Signup.objects.all() + queryset = Signup.objects.filter(deleted=False) serializer_class = SignupSerializer permission_classes = [SignupPermission] @@ -156,6 +156,15 @@ class SignupViewSet(ModelViewSet): else: return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) + def destroy(self, request, pk=None, *args, **kwargs): + try: + signup = self.get_object() + signup.deleted = True + signup.save() + return JsonResponse(status=200, data={"message": "OK"}) + except ObjectDoesNotExist: + return JsonResponse(status=404, data={"error": f"Signup {pk} not found"}) + class SavedQuestionsViewSet(ModelViewSet): queryset = TemplateQuestion.objects.all() From 00c6920c8ad88a46ffd049c3eacf29a0f8837a79 Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Sat, 7 Nov 2020 21:00:55 +0200 Subject: [PATCH 2/6] Add test for editing signup with uuid --- webapp/tests/test_signup.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webapp/tests/test_signup.py b/webapp/tests/test_signup.py index 5f7f421..6184867 100644 --- a/webapp/tests/test_signup.py +++ b/webapp/tests/test_signup.py @@ -69,11 +69,15 @@ class SignupTestCase(APITestCase): def test_get_hidden_forms_admin(self): pass - # Update and Delete are available for super admin (Django Admin) - # and to the user that signed up (uid token) - @skip("NotImplemented") def test_update_signup_token(self): - pass + id = self.signup1.id + uuid = self.signup1.uuid + clone = ALL_QUESTION_TYPES_ANSWER.copy() + clone["-naY2R1-h"] = "Edited Testi" + new = createSignupRequest("asd", self.signupForm.id, clone) + response = self.client.put(f"{URL}{id}/edit/?uuid={uuid}", new, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(Signup.objects.get(id=id).answer["-naY2R1-h"], "Edited Testi") @skip("NotImplemented") def test_delete_signup_token(self): From b1921d4926e34e0d338b9875ea438a3020a7b63a Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Sat, 7 Nov 2020 21:32:49 +0200 Subject: [PATCH 3/6] Add API for sending emails for signupees --- webapp/tests/signup_fixture.py | 5 +++-- webapp/tests/test_signup.py | 15 +++++++++++++++ webapp/utils.py | 4 ++++ webapp/views.py | 24 +++++++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/webapp/tests/signup_fixture.py b/webapp/tests/signup_fixture.py index fecef61..a15b5fd 100644 --- a/webapp/tests/signup_fixture.py +++ b/webapp/tests/signup_fixture.py @@ -40,14 +40,15 @@ CBOX_SCHEMA = { } -def createSignupForm(name="Form1", start_time=timezone.now(), end_time=month_from_now(), questions=ALL_QUESTION_TYPES, schema=ALL_QUESTIONS_SCHEMA, visible=True): +def createSignupForm(name="Form1", start_time=timezone.now(), end_time=month_from_now(), questions=ALL_QUESTION_TYPES, schema=ALL_QUESTIONS_SCHEMA, visible=True, quota=1): return SignupForm.objects.create( title=name, start_time=start_time, end_time=end_time, questions=questions, visible=visible, - schema=schema + schema=schema, + quota=quota ) diff --git a/webapp/tests/test_signup.py b/webapp/tests/test_signup.py index 6184867..7ac905e 100644 --- a/webapp/tests/test_signup.py +++ b/webapp/tests/test_signup.py @@ -82,3 +82,18 @@ class SignupTestCase(APITestCase): @skip("NotImplemented") def test_delete_signup_token(self): pass + + # TODO: Use some mocking library and check that mailjet is actually called + def test_signupee_sendemail(self): + form = self.signupForm + emailURL = f"/api/signupForm/{form.id}/sendemail/" + payload = { + "subject": "Email subject", + "content": "Markdown", + "mode": "actual" + } + no_auth_response = self.client.post(emailURL, payload, format="json") + self.assertEqual(no_auth_response.status_code, status.HTTP_401_UNAUTHORIZED) + self.client.force_authenticate(user=self.authClient) + response = self.client.post(emailURL, payload, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) diff --git a/webapp/utils.py b/webapp/utils.py index 1f60f3a..37205b7 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -101,3 +101,7 @@ def send_signup_email(to, subject, id, uuid, content): ) return send_email(to, subject, message, True) + +def admin_send_email_signupees(list, subject, content): + for to in list: + send_email(to, subject, markdown.markdown(content), True) diff --git a/webapp/views.py b/webapp/views.py index 3800daa..722cc9d 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -21,7 +21,7 @@ from jsonschema.exceptions import ValidationError from webapp.models import * from webapp.serializers import * -from webapp.utils import decode_base64_file +from webapp.utils import admin_send_email_signupees, decode_base64_file class SignupPermission(BasePermission): @@ -105,6 +105,28 @@ class SignupFormViewSet(ModelViewSet): return SignupForm.objects.filter(visible=True, end_time__gt=timezone.now()).order_by('start_time') + @action(detail=True, methods=['post']) + def sendemail(self, request, pk=None, *args, **kwargs): + subject = request.data["subject"] + content = request.data["content"] + mode = request.data["mode"] + + queryset = self.filter_queryset(self.get_queryset()) + filter = {'pk': pk} + signupForm = get_object_or_404(queryset, **filter) + if (mode == "all"): + admin_send_email_signupees(signupForm.signups, subject, content) + return JsonResponse(status=201, data={"message": "Email sent"}) + elif (mode == "actual"): + admin_send_email_signupees(signupForm.signups, subject, content) + return JsonResponse(status=201, data={"message": "Email sent"}) + elif (mode == "reserved"): + admin_send_email_signupees(signupForm.signups, subject, content) + return JsonResponse(status=201, data={"message": "Email sent"}) + else: + return JsonResponse(status=400, data={"error": f"Bad mode '{mode}'"}) + + class SignupViewSet(ModelViewSet): queryset = Signup.objects.filter(deleted=False) serializer_class = SignupSerializer From b834567309ec18e9652e7be8433178d0ab94a9ef Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Sun, 8 Nov 2020 00:16:29 +0200 Subject: [PATCH 4/6] API for getting signups for list view --- webapp/models.py | 2 +- webapp/views.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/webapp/models.py b/webapp/models.py index 3349e25..36be46f 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -115,7 +115,7 @@ class SignupForm(models.Model): @property def signups(self): - return Signup.objects.filter(signupForm=self).order_by('pk') + return Signup.objects.filter(signupForm=self, deleted=False).order_by('pk') @property def isOpen(self): diff --git a/webapp/views.py b/webapp/views.py index 722cc9d..75adb41 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -12,10 +12,11 @@ from django_filters import rest_framework as filters from django.db.models import Prefetch from django.core.exceptions import ObjectDoesNotExist from rest_framework import routers +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission, AllowAny +from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission, AllowAny, IsAuthenticated from jsonschema import validate from jsonschema.exceptions import ValidationError @@ -105,7 +106,7 @@ class SignupFormViewSet(ModelViewSet): return SignupForm.objects.filter(visible=True, end_time__gt=timezone.now()).order_by('start_time') - @action(detail=True, methods=['post']) + @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated]) def sendemail(self, request, pk=None, *args, **kwargs): subject = request.data["subject"] content = request.data["content"] @@ -118,15 +119,23 @@ class SignupFormViewSet(ModelViewSet): admin_send_email_signupees(signupForm.signups, subject, content) return JsonResponse(status=201, data={"message": "Email sent"}) elif (mode == "actual"): - admin_send_email_signupees(signupForm.signups, subject, content) + admin_send_email_signupees(signupForm.signups[:signupForm.quota], subject, content) return JsonResponse(status=201, data={"message": "Email sent"}) elif (mode == "reserved"): - admin_send_email_signupees(signupForm.signups, subject, content) + admin_send_email_signupees(signupForm.signups[signupForm.quota:], subject, content) return JsonResponse(status=201, data={"message": "Email sent"}) else: return JsonResponse(status=400, data={"error": f"Bad mode '{mode}'"}) + @action(detail=True, methods=['get'], permission_classes=[IsAuthenticated]) + def signups(self, request, pk=None, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + filter = {'pk': pk} + signupForm = get_object_or_404(queryset, **filter) + return Response(SignupSerializer(signupForm.signups, many=True).data) + + class SignupViewSet(ModelViewSet): queryset = Signup.objects.filter(deleted=False) serializer_class = SignupSerializer From e5224fe1e2b4f008b771f111bd4e1608f9bdf8e9 Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Mon, 23 Nov 2020 20:23:44 +0200 Subject: [PATCH 5/6] Lint fix --- webapp/utils.py | 1 + webapp/views.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/utils.py b/webapp/utils.py index 37205b7..71de738 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -102,6 +102,7 @@ def send_signup_email(to, subject, id, uuid, content): return send_email(to, subject, message, True) + def admin_send_email_signupees(list, subject, content): for to in list: send_email(to, subject, markdown.markdown(content), True) diff --git a/webapp/views.py b/webapp/views.py index 75adb41..52cfda7 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -105,7 +105,6 @@ class SignupFormViewSet(ModelViewSet): return SignupForm.objects.all().order_by('start_time') return SignupForm.objects.filter(visible=True, end_time__gt=timezone.now()).order_by('start_time') - @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated]) def sendemail(self, request, pk=None, *args, **kwargs): subject = request.data["subject"] @@ -127,7 +126,6 @@ class SignupFormViewSet(ModelViewSet): else: return JsonResponse(status=400, data={"error": f"Bad mode '{mode}'"}) - @action(detail=True, methods=['get'], permission_classes=[IsAuthenticated]) def signups(self, request, pk=None, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) From c5008e3656b8ca0a7ea5e52583937a86ad4eca5a Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Mon, 30 Nov 2020 20:54:31 +0200 Subject: [PATCH 6/6] Add debug logging --- webapp/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/utils.py b/webapp/utils.py index 71de738..03024d7 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -55,6 +55,10 @@ def month_from_now(): def send_email(to, subject, body, html=False): if not ENABLE_AUTOMATIC_EMAILS: + logging.debug("Skipping email") + logging.debug(f"to: {to}") + logging.debug(f"subject: {subject}") + logging.debug(f"body: {body}") return try: mailjet = Client(auth=(EMAIL_API_KEY, EMAIL_API_SECRET), version='v3.1')