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..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): @@ -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/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 4ec1ef8..7ac905e 100644 --- a/webapp/tests/test_signup.py +++ b/webapp/tests/test_signup.py @@ -56,16 +56,44 @@ 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 - # 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): 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..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') @@ -101,3 +105,8 @@ 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 87f2d0d..52cfda7 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -12,16 +12,17 @@ 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 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): @@ -104,9 +105,37 @@ 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"] + 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[:signupForm.quota], subject, content) + return JsonResponse(status=201, data={"message": "Email sent"}) + elif (mode == "reserved"): + 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.all() + queryset = Signup.objects.filter(deleted=False) serializer_class = SignupSerializer permission_classes = [SignupPermission] @@ -156,6 +185,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()