Skip to content
Snippets Groups Projects
Commit 0fcd23c0 authored by florimondmanca's avatar florimondmanca
Browse files

refactor mails templates, update participation cancelled endpoint, add mass...

refactor mails templates, update participation cancelled endpoint, add mass accept/reject actions in participation admin
parent ea733cef
Branches
No related tags found
No related merge requests found
...@@ -49,23 +49,18 @@ class ParticipationEndpointsTest(HyperlinkedAPITestCase): ...@@ -49,23 +49,18 @@ class ParticipationEndpointsTest(HyperlinkedAPITestCase):
expected_status_code=status.HTTP_204_NO_CONTENT) expected_status_code=status.HTTP_204_NO_CONTENT)
class AbandonTest(APITestCase): class ParticipationCancelledTest(APITestCase):
"""Test endpoint to notify a user does not participate to visit anymore.""" """Test endpoint to notify a user does not participate to visit anymore."""
def setUp(self): def setUp(self):
self.user = UserFactory.create() self.user = UserFactory.create()
self.visit = VisitFactory.create() VisitFactory.create()
self.reason = ( self.obj = ParticipationFactory.create()
"Désolé, je ne peux plus venir à cause d'un rendez-vous médical.")
def perform(self): def perform(self):
data = { data = {'reason': 'Désolé, je ne peux plus venir.'}
'user': self.user.pk, url = f'/api/participations/{self.obj.pk}/notify_cancelled/'
'visit': self.visit.pk, response = self.client.post(url, data=data, format='json')
'reason': self.reason,
}
response = self.client.post('/api/participations/abandon/', data=data,
format='json')
return response return response
def test(self): def test(self):
......
"""Visits admin panel configuration.""" """Visits admin panel configuration."""
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin, messages
from .models import Visit, Place, Participation from django.template.defaultfilters import pluralize
from .models import Participation, Place, Visit
# Register your models here. # Register your models here.
...@@ -71,12 +73,43 @@ class ParticipationInline(admin.StackedInline): ...@@ -71,12 +73,43 @@ class ParticipationInline(admin.StackedInline):
extra = 0 extra = 0
def accept_selected_participations(modeladmin, request, queryset):
"""Accept selected participations in list view."""
for obj in queryset:
obj.accepted = True
obj.save()
count = queryset.count()
s = pluralize(count)
messages.add_message(request, messages.SUCCESS,
f'{count} participation{s} acceptée{s} avec succès.')
accept_selected_participations.short_description = (
'Accepter les participations sélectionnées')
def reject_selected_participations(modeladmin, request, queryset):
"""Reject selected participations in list view."""
for obj in queryset:
obj.accepted = False
obj.save()
count = queryset.count()
s = pluralize(count)
messages.add_message(request, messages.SUCCESS,
f'{count} participation{s} acceptée{s} avec succès.')
reject_selected_participations.short_description = (
'Rejeter les participations sélectionnées')
@admin.register(Participation) @admin.register(Participation)
class ParticipationAdmin(admin.ModelAdmin): class ParticipationAdmin(admin.ModelAdmin):
"""Admin panel for visit participations.""" """Admin panel for visit participations."""
list_display = ('visit', 'user', 'accepted', 'present') list_display = ('visit', 'user', 'accepted', 'present')
list_filter = ('visit',) list_filter = ('visit',)
actions = [accept_selected_participations, reject_selected_participations]
@admin.register(Visit.organizers.through) @admin.register(Visit.organizers.through)
......
...@@ -11,10 +11,10 @@ from .models import Visit ...@@ -11,10 +11,10 @@ from .models import Visit
User = get_user_model() User = get_user_model()
class Abandon(Notification): class ParticipationCancelled(Notification):
args = ('user', 'user_email', 'visit', 'date', 'reason',) args = ('user', 'visit', 'reason',)
template_name = 'mails/visits/abandon.md' template_name = 'visits/participation_cancelled.md'
recipients = [settings.VISITS_TEAM_EMAIL] recipients = [settings.VISITS_TEAM_EMAIL]
def get_subject(self): def get_subject(self):
...@@ -22,14 +22,14 @@ class Abandon(Notification): ...@@ -22,14 +22,14 @@ class Abandon(Notification):
@classmethod @classmethod
def example(cls): def example(cls):
return cls(user='John Doe', user_email='john.doe@example.com', return cls(user='John Doe',
visit='Visite du Palais de la Découverte', visit='Visite du Palais de la Découverte',
date=now(), reason='Je ne peux plus venir...') reason='Je ne peux plus venir...')
class Participation(Notification): class Participation(Notification):
template_name = 'mails/visits/participation.md' template_name = 'visits/participation.md'
args = ('user', 'visit',) args = ('user', 'visit',)
accepted: bool accepted: bool
......
...@@ -9,8 +9,8 @@ from profiles.models import Tutor ...@@ -9,8 +9,8 @@ from profiles.models import Tutor
from users.models import User from users.models import User
from users.serializers import UserSerializer from users.serializers import UserSerializer
from .notifications import ParticipationCancelled
from .models import Participation, Place, Visit from .models import Participation, Place, Visit
from .notifications import Abandon
class PlaceSerializer(serializers.ModelSerializer): class PlaceSerializer(serializers.ModelSerializer):
...@@ -129,35 +129,23 @@ class VisitSerializer(VisitListSerializer): ...@@ -129,35 +129,23 @@ class VisitSerializer(VisitListSerializer):
'url',) 'url',)
class AbandonSerializer(serializers.Serializer): class ParticipationCancelledSerializer(serializers.Serializer):
"""Serializer for abandon notifications.""" """Serializer for sending participation cancelled notifications."""
user = serializers.PrimaryKeyRelatedField(
write_only=True,
queryset=User.objects.all(),
label='User',
help_text='ID of the user who does not participate anymore.')
reason = serializers.CharField( reason = serializers.CharField(
write_only=True, write_only=True,
label='Reason', label='Reason',
help_text='An explanation of why the user abandonned.') help_text='Explain why the user cancelled their participation.')
visit = serializers.PrimaryKeyRelatedField(
write_only=True,
label='Visit',
help_text='ID of the visit the user has quit.',
queryset=Visit.objects.all())
sent = serializers.BooleanField(read_only=True) sent = serializers.BooleanField(read_only=True)
timestamp = serializers.DateTimeField(read_only=True) timestamp = serializers.DateTimeField(read_only=True)
def create(self, validated_data): def update(self, instance: Participation, validated_data):
user = validated_data['user'] """Send a notification as the user cancelled their participation."""
visit = validated_data['visit']
reason = validated_data['reason'] reason = validated_data['reason']
notification = Abandon( notification = ParticipationCancelled(
user=user, user_email=user.email, visit=visit, user=instance.user, visit=instance.visit, reason=reason)
date=visit.date, reason=reason)
notification.send() notification.send()
return notification return notification
......
...@@ -16,7 +16,7 @@ def fire_accepted_changed(sender, instance: Participation, created, **kwargs): ...@@ -16,7 +16,7 @@ def fire_accepted_changed(sender, instance: Participation, created, **kwargs):
accepted_changed.send(sender=sender, instance=instance) accepted_changed.send(sender=sender, instance=instance)
@receiver(accepted_changed, sender=Participation) @receiver(accepted_changed)
def notify_participation(sender, instance: Participation, **kwargs): def notify_participation(sender, instance: Participation, **kwargs):
"""Send notification to user depending on their participation status. """Send notification to user depending on their participation status.
......
{% extends 'mails/notification.md' %} {% extends 'mails/notification.md' %}
{% block greeting %} {% block greeting %}
Bonjour {{ user.first_name }}, Bonjour{% if user.first_name %} {{ user.first_name }}{% endif %},
{% endblock %} {% endblock %}
{% block body %} {% block body %}
Tu as demandé à t'inscrire à la sortie **{{ visit.title }}** organisée le **{{ visit.date|date }}**. Tu as demandé à t'inscrire à la sortie **{{ visit.title }}** organisée le **{{ visit.date|date }}**.
{% if accepted %} {% if accepted %}
Nous avons validé ta participation à la sortie. ✅ Bonne nouvelle : nous avons validé ta participation à la sortie. ✅
En te rendant sur [l'espace sorties]({{ visit.get_site_url }}), tu peux dès à présent: En te rendant sur [l'espace sorties]({{ visit.get_site_url }}), tu peux dès à présent:
...@@ -18,13 +18,13 @@ En te rendant sur [l'espace sorties]({{ visit.get_site_url }}), tu peux dès à ...@@ -18,13 +18,13 @@ En te rendant sur [l'espace sorties]({{ visit.get_site_url }}), tu peux dès à
{% else %} {% else %}
Malheureusement, en raison du nombre de places limité, tu ne pourras pas participer à cette sortie. 😔 Malheureusement, en raison du nombre de places limité, tu ne pourras pas participer à cette sortie. 😔
Nous te recontacterons si des places se libèrent suite à des désistements. Nous te recontacterons si des places se libèrent suite à des désistements.
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block signature %} {% block signature %}
À bientôt, À très bientôt,
Les organisateurs Les organisateurs
Nous contacter : oser.sortie@gmail.fr Nous contacter : oser.sortie@gmail.fr
......
{% extends 'mails/notification.md' %} {% extends 'mails/notification.md' %}
{% block body %} {% block body %}
{{ user }} s'est désinscrit(e) de la sortie **{{ visit }}** organisée le **{{ date|date }}**. Voici la raison de son désistement : {{ user }} s'est désinscrit(e) de la sortie **{{ visit }}** organisée le **{{ visit.date|date }}**. Voici la raison de son désistement :
> {{ reason }} > {{ reason }}
Vous pouvez contacter {{ user }} grâce à son adresse mail : {{ user_email }}. Vous pouvez contacter {{ user }} grâce à son adresse mail : {{ user.email }}.
{% endblock %} {% endblock %}
...@@ -7,7 +7,7 @@ from rest_framework.decorators import action ...@@ -7,7 +7,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from .models import Participation, Place, Visit from .models import Participation, Place, Visit
from .serializers import (AbandonSerializer, ParticipationSerializer, from .serializers import (ParticipationCancelledSerializer, ParticipationSerializer,
PlaceSerializer, VisitListSerializer, PlaceSerializer, VisitListSerializer,
VisitSerializer) VisitSerializer)
...@@ -174,35 +174,39 @@ class ParticipationsViewSet(mixins.CreateModelMixin, ...@@ -174,35 +174,39 @@ class ParticipationsViewSet(mixins.CreateModelMixin,
permission_classes = [DRYPermissions] permission_classes = [DRYPermissions]
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'abandon': if self.action == 'notify_cancelled':
return AbandonSerializer return ParticipationCancelledSerializer
return ParticipationSerializer return ParticipationSerializer
@action(methods=['post'], detail=False) @action(methods=['post'], detail=True)
def abandon(self, request): def notify_cancelled(self, request, pk=True):
"""Notify the visits team that a student abandoned a participation. """Notify the visits team that a user cancelled their participation.
An email will be sent to the visits team's email address. An email will be sent to the visits team's email address.
You should:
- Call this endpoint **before** you delete the associated
participation.
- Only call this endpoint if the participation has `accepted = True`,
as this is not checked for.
### Example payload ### Example payload
{ {
"user": 3, "reason": "Désolé, je ne peux plus venir…"
"reason": "Désolé, je ne peux plus venir…",
"visit": 1
} }
### Example response ### Example response
{ {
"user": "John Doe",
"reason": "Désolé, je ne peux plus venir…",
"visit": "Visite du Palais de la Découverte",
"sent": "2018-05-23 22:27:06.313137+00:00", "sent": "2018-05-23 22:27:06.313137+00:00",
"recipient": "oser.geek@gmail.com" "recipient": "oser.geek@gmail.com"
} }
""" """
serializer = AbandonSerializer(data=request.data) participation = self.get_object()
serializer = ParticipationCancelledSerializer(
participation, data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response( return Response(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment