From 26af46fa12a67bb177c0580692528895933ce089 Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Mon, 22 Jun 2020 23:09:20 +0300 Subject: [PATCH] UUID, email fields and receiver for sending them, /edit API for modifying signup with ID and UUID --- webapp/migrations/0064_signup_uuid.py | 19 ++++++++ webapp/migrations/0065_signup_email.py | 18 ++++++++ webapp/migrations/0066_auto_20200622_2302.py | 18 ++++++++ webapp/models.py | 38 +++++++++++++--- webapp/serializers.py | 18 ++++++++ webapp/utils.py | 16 ++++++- webapp/views.py | 47 ++++++++++++++------ 7 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 webapp/migrations/0064_signup_uuid.py create mode 100644 webapp/migrations/0065_signup_email.py create mode 100644 webapp/migrations/0066_auto_20200622_2302.py diff --git a/webapp/migrations/0064_signup_uuid.py b/webapp/migrations/0064_signup_uuid.py new file mode 100644 index 0000000..0050252 --- /dev/null +++ b/webapp/migrations/0064_signup_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.5 on 2020-06-22 15:42 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0063_signup_list_name'), + ] + + operations = [ + migrations.AddField( + model_name='signup', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + ] diff --git a/webapp/migrations/0065_signup_email.py b/webapp/migrations/0065_signup_email.py new file mode 100644 index 0000000..7a04475 --- /dev/null +++ b/webapp/migrations/0065_signup_email.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2020-06-22 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0064_signup_uuid'), + ] + + operations = [ + migrations.AddField( + model_name='signup', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + ] diff --git a/webapp/migrations/0066_auto_20200622_2302.py b/webapp/migrations/0066_auto_20200622_2302.py new file mode 100644 index 0000000..ad4873f --- /dev/null +++ b/webapp/migrations/0066_auto_20200622_2302.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2020-06-22 20:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0065_signup_email'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='signupForm', + field=models.ManyToManyField(blank=True, related_name='event', to='webapp.SignupForm'), + ), + ] diff --git a/webapp/models.py b/webapp/models.py index 4f5d99d..87b60ab 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -1,21 +1,23 @@ """Webapp app models.""" +from django.conf import settings from django.db import models from django.utils import timezone # from datetime import timedelta from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver -from webapp.utils import month_from_now +from webapp.utils import month_from_now, send_signup_email from django.utils.translation import ugettext_lazy as _ from auditlog.registry import auditlog from phonenumber_field.modelfields import PhoneNumberField from django.contrib.postgres.fields import JSONField - -# import logging - +from uuid import uuid4 +import logging +from smtplib import SMTPAuthenticationError VERBOSE_NAME = _('Webapp') +EMAIL_REGEX = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" class Tag(models.Model): @@ -64,7 +66,7 @@ class Event(BaseFeed): start_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now) signupForm = models.ManyToManyField( - 'SignupForm', blank=True) + 'SignupForm', blank=True, related_name="event") location = models.CharField(max_length=255, blank=True) def __str__(self): @@ -116,6 +118,13 @@ class SignupForm(models.Model): properties[id] = { "type": "string" } + elif question_type == "email": + # Format is just a "FYI" field, so we also have pattern. + properties[id] = { + "type": "string", + "format": "email", + "pattern": EMAIL_REGEX + } elif question_type == "radiobutton": options = q["options"] regexes = map(lambda x: f"^{x}$", options) @@ -163,6 +172,10 @@ class Signup(models.Model): answer = JSONField() # Answer we use in signupForm signups field. Frontend uses first questions answer as this value. list_name = models.CharField(_('Name'), max_length=255) + # If there is email in questions, we save it as own field + email = models.EmailField(blank=True, null=True) + # Random unique identifier. Used for signup editing by the user. + uuid = models.UUIDField(default=uuid4, editable=False) def __str__(self): return f"{self.signupForm}: {self.list_name} ({self.pk})" @@ -172,6 +185,21 @@ class Signup(models.Model): verbose_name_plural = _('Sign-ups') +@receiver(post_save, sender=Signup) +def email_on_singup(sender, instance, created, **kwargs): + """Send email validation.""" + if not settings.ENABLE_AUTOMATIC_EMAILS: + return + + try: + if created and instance.email: + # TODO: Possible bug due to many-to-many relationship with events and forms. + subject = _(f"Olet ilmoittautunut tapahtumaan {instance.signupForm.event.first().title}") + send_signup_email(instance.email, subject, instance.id, instance.uuid) + except SMTPAuthenticationError: + logging.error('Failed to send email to signup') + + class BaseRole(models.Model): """Base model for occupations/roles.""" diff --git a/webapp/serializers.py b/webapp/serializers.py index 938bc6d..d034814 100644 --- a/webapp/serializers.py +++ b/webapp/serializers.py @@ -8,6 +8,24 @@ class SignupSerializer(serializers.ModelSerializer): queryset=SignupForm.objects.all() ) answer = serializers.JSONField() + list_name = serializers.CharField(read_only=True) + + def add_extra_fields(self, validated_data): + questions = validated_data["signupForm"].questions + validated_data["list_name"] = validated_data["answer"].get(questions[0]["id"], "") + + email_fields = list(filter(lambda x: x["type"] == "email", questions)) + if (len(email_fields) > 0): + email_value = validated_data["answer"].get(email_fields[0]["id"], None) + validated_data["email"] = email_value + + def create(self, validated_data): + self.add_extra_fields(validated_data) + return super().create(validated_data) + + def update(self, instance, validated_data): + self.add_extra_fields(validated_data) + return super().update(instance, validated_data) class Meta: model = Signup diff --git a/webapp/utils.py b/webapp/utils.py index 5e0cdd2..25d6bc0 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -6,6 +6,8 @@ from django.core.mail import send_mail from datetime import timedelta import logging from django.conf import settings +from django.template.loader import render_to_string +from sikweb.settings import URL def month_from_now(): @@ -13,17 +15,27 @@ def month_from_now(): return timezone.now() + timedelta(days=30) -def send_email(to, subject, body): +def send_email(to, subject, body, fail_silently=False): try: success = send_mail( subject, body, settings.DEFAULT_EMAIL_FROM, [to], - fail_silently=False, + fail_silently=fail_silently, ) if success == 0: raise Exception('Failed to send email!') except Exception as ex: logging.exception('Failed to send email.') + + +def send_signup_email(to, subject, id, uuid): + message = render_to_string( + 'webapp:signup_email.html', { + 'url': f"https://{URL}/api/signup/{id}/edit/?uuid={uuid}", + } + ) + + return send_email(to, subject, message, fail_silently=True) diff --git a/webapp/views.py b/webapp/views.py index 2961794..c07b303 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -6,13 +6,14 @@ from django.utils import timezone from dealer.git import git from django.conf import settings from django.http import HttpResponse, JsonResponse -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from django.views.decorators.http import require_http_methods from django_filters import rest_framework as filters from django.core.exceptions import ObjectDoesNotExist from rest_framework import routers, viewsets from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission, AllowAny from jsonschema import validate from jsonschema.exceptions import ValidationError @@ -22,7 +23,7 @@ from webapp.serializers import (EventSerializer, SignupFormSerializer, SignupSer OccupationSerializer, TagSerializer) -class IsPostOrIsAuthenticated(BasePermission): +class SignupPermission(BasePermission): def has_permission(self, request, view): if request.method == 'POST': @@ -58,10 +59,6 @@ class SignupFormViewSet(viewsets.ModelViewSet): queryset = SignupForm.objects.all() serializer_class = SignupFormSerializer permission_classes = [IsAuthenticatedOrReadOnly] - # Throws errors with JSONFIeld. Modify __all__ to not use JSONField if filters are enadbled - # filter_backends = (filters.DjangoFilterBackend, SearchFilter, OrderingFilter) - # filter_fields = '__all__' - # search_fields = '__all__' def create(self, request, *args, **kwargs): try: @@ -82,10 +79,18 @@ class SignupFormViewSet(viewsets.ModelViewSet): class SignupViewSet(viewsets.ModelViewSet): queryset = Signup.objects.all() serializer_class = SignupSerializer - permission_classes = [IsPostOrIsAuthenticated] - # filter_backends = (filters.DjangoFilterBackend, SearchFilter, OrderingFilter) - # filter_fields = '__all__' - # search_fields = '__all__' + permission_classes = [SignupPermission] + + @action(detail=True, methods=['get', 'post'], permission_classes=[AllowAny]) + def edit(self, request, pk=None, *args, **kwargs): + uuid = request.query_params.get("uuid", None) + queryset = self.filter_queryset(self.get_queryset()) + filter = {'pk': pk, 'uuid': uuid} + signup = get_object_or_404(queryset, **filter) + if request.method == 'GET': + return self.retrieve(request, *args, **kwargs) + elif request.method == 'POST': + return self.partial_update(request, *args, **kwargs) def create(self, request, *args, **kwargs): id = request.data["signupForm_id"] @@ -103,8 +108,24 @@ class SignupViewSet(viewsets.ModelViewSet): else: return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) - def update(self, request, *args, **kwargs): - return super().update(request, *args, **kwargs) + def partial_update(self, request, pk=None, *args, **kwargs): + try: + # ID & UUID validated in edit @action for normal users. + # This is otherwise open for authenticated users. + signup = self.get_object() + answer = request.data["answer"] + form = SignupForm.objects.get(id=signup.signupForm_id) + + if (form.visible): + # Throws ValidationError if not valid + validate(instance=answer, schema=form.schema) + return super().partial_update(request, *args, **kwargs) + except ValidationError as inst: + return JsonResponse(status=400, data={"error": inst.message}) + except ObjectDoesNotExist: + return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) + else: + return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) class SavedQuestionsViewSet(viewsets.ModelViewSet):