UUID, email fields and receiver for sending them, /edit API for modifying signup with ID and UUID

This commit is contained in:
Aarni Halinen
2020-06-22 23:09:20 +03:00
parent 027b9c370c
commit 26af46fa12
7 changed files with 154 additions and 20 deletions
+19
View File
@@ -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),
),
]
+18
View File
@@ -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),
),
]
@@ -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'),
),
]
+33 -5
View File
@@ -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."""
+18
View File
@@ -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
+14 -2
View File
@@ -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)
+34 -13
View File
@@ -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):