Skip to content
Snippets Groups Projects
Commit d45529e5 authored by HeJ's avatar HeJ
Browse files

Merge branch 'feature/app_api' into 'develop'

API for DMs + Liked events

See merge request hub/hub!954
parents 2920d250 e888b19c
Branches
No related tags found
No related merge requests found
......@@ -11,9 +11,10 @@ from core.models.assemblies import Assembly
from core.models.badges import Badge, BadgeToken, BadgeTokenTimeConstraint
from core.models.conference import Conference, ConferenceMember, ConferenceTrack
from core.models.events import Event
from core.models.messages import DirectMessage
from core.models.metanavi import MetaNavItem
from core.models.rooms import Room
from core.models.users import UserTimelineEntry
from core.models.users import PlatformUser, UserTimelineEntry
class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField):
......@@ -77,6 +78,12 @@ class HubModelSerializer(ValidatingModelSerializer):
staff_only_fields = None
# now check the user's conference membership based on the request
if 'request' not in self.context:
from django.contrib.auth.models import AnonymousUser
self.request_user = request_user = AnonymousUser()
self.conference_member = None
else:
self.request_user = request_user = self.context['request'].user
self.conference_member = None
if request_user.is_authenticated:
......@@ -297,3 +304,68 @@ class MetaNavItemSerializer(HubModelSerializer):
'graphic_light',
'graphic_dark',
]
class PlatformUserVisibleSerializer(serializers.ModelSerializer):
class Meta:
model = PlatformUser
fields = ['username', 'display_name']
read_only_fields = fields
class PlatformUserByUsernameFieldSerializer(serializers.SlugRelatedField):
def __init__(self, *args, **kwargs):
super().__init__(*args, slug_field='username', **kwargs)
def get_queryset(self):
return PlatformUser.objects.all()
class DirectMessageSerializerSentShort(HubModelSerializer):
class Meta:
model = DirectMessage
fields = ['id', 'sender', 'recipient', 'timestamp', 'in_reply_to', 'subject']
read_only_fields = fields
sender = PlatformUserVisibleSerializer(read_only=True)
recipient = PlatformUserVisibleSerializer(read_only=True)
class DirectMessageSerializerReceivedShort(HubModelSerializer):
class Meta:
model = DirectMessage
fields = [*DirectMessageSerializerSentShort.Meta.fields, 'has_responded']
read_only_fields = fields
sender = PlatformUserVisibleSerializer(read_only=True)
recipient = PlatformUserVisibleSerializer(read_only=True)
class DirectMessageSerializerSent(HubModelSerializer):
class Meta:
model = DirectMessage
fields = [*DirectMessageSerializerSentShort.Meta.fields, 'body']
read_only_fields = fields
sender = PlatformUserVisibleSerializer(read_only=True)
recipient = PlatformUserVisibleSerializer(read_only=True)
class DirectMessageSerializerReceived(HubModelSerializer):
class Meta:
model = DirectMessage
fields = [*DirectMessageSerializerReceivedShort.Meta.fields, 'body']
read_only_fields = [f for f in fields if f != 'was_read']
sender = PlatformUserVisibleSerializer(read_only=True)
recipient = PlatformUserVisibleSerializer(read_only=True)
class DirectMessageSendSerializer(HubModelSerializer):
class Meta:
model = DirectMessage
fields = ['id', 'recipient', 'subject', 'body']
read_only_fields = ['id']
write_only_fields = [f for f in fields if f != 'id']
recipient = PlatformUserByUsernameFieldSerializer()
from .badges import * # noqa: F401, F403
from .bbb import * # noqa: F401, F403
from .engelsystem import * # noqa: F401, F403
from .events import * # noqa: F401, F403
from .map import * # noqa: F401, F403
from .messages import * # noqa: F401, F403
from .metrics import * # noqa: F401, F403
from .schedule import * # noqa: F401, F403
from .workadventure import * # noqa: F401, F403
......
import uuid
from datetime import datetime, timedelta
from pytz import UTC
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from core.models import Assembly, Conference, ConferenceMember, Event, PlatformUser
TEST_CONF_ID = uuid.uuid4()
@override_settings(SELECTED_CONFERENCE_ID=TEST_CONF_ID)
class EventsTestCase(TestCase):
def setUp(self):
self.conference1 = Conference(id=TEST_CONF_ID, slug='foo', name='Foo Conference', is_public=True)
self.conference1.save()
self.human_user = PlatformUser(username='bernd', user_type=PlatformUser.Type.HUMAN)
self.human_user.save()
self.human_cm = ConferenceMember(conference=self.conference1, user=self.human_user)
self.human_cm.save()
assembly = Assembly(conference=self.conference1, name='DUMMY', slug='dummy', state_assembly=Assembly.State.ACCEPTED, is_official=True)
assembly.save()
self.event = Event(
conference=self.conference1,
slug='event1',
assembly=assembly,
name='Event1_1',
is_public=True,
schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC),
schedule_duration=timedelta(minutes=45),
kind=Event.Kind.OFFICIAL,
)
self.event.save()
def test_EventMyFavorites(self):
c = Client()
resp = c.get(reverse('api:my-events'))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user)
resp = c.get(reverse('api:my-events'))
self.assertEqual(resp.json(), [])
self.human_user.favorite_events.add(self.event)
resp = c.get(reverse('api:my-events'))
self.assertEqual(resp.json(), [str(self.event.pk)])
def test_EventMyFavoritesSet_put(self):
c = Client()
resp = c.put(reverse('api:my-events-set', kwargs={'pk': str(uuid.uuid4())}))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user)
self.assertEqual(list(self.human_user.favorite_events.all()), [])
resp = c.put(reverse('api:my-events-set', kwargs={'pk': str(self.event.pk)}))
self.assertEqual(list(self.human_user.favorite_events.all()), [self.event])
def test_EventMyFavoritesSet_delete(self):
c = Client()
resp = c.delete(reverse('api:my-events-set', kwargs={'pk': str(uuid.uuid4())}))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user)
self.human_user.favorite_events.add(self.event)
self.assertEqual(list(self.human_user.favorite_events.all()), [self.event])
resp = c.delete(reverse('api:my-events-set', kwargs={'pk': str(self.event.pk)}))
self.assertEqual(list(self.human_user.favorite_events.all()), [])
import uuid
from datetime import datetime
from freezegun import freeze_time
from pytz import UTC
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from django.utils.timezone import localtime
from core.models import Conference, ConferenceMember, DirectMessage, PlatformUser
TEST_CONF_ID = uuid.uuid4()
@override_settings(SELECTED_CONFERENCE_ID=TEST_CONF_ID)
class MessagesTestCase(TestCase):
def setUp(self):
self.conference1 = Conference(id=TEST_CONF_ID, slug='foo', name='Foo Conference', is_public=True)
self.conference1.save()
self.human_user1 = PlatformUser(username='bernd', user_type=PlatformUser.Type.HUMAN)
self.human_user1.save()
self.human_cm1 = ConferenceMember(conference=self.conference1, user=self.human_user1)
self.human_cm1.save()
self.human_user2 = PlatformUser(username='björnd', user_type=PlatformUser.Type.HUMAN)
self.human_user2.save()
self.human_cm2 = ConferenceMember(conference=self.conference1, user=self.human_user2)
self.human_cm2.save()
def test_DirectMessagesReceived(self):
c = Client()
resp = c.get(reverse('api:my-messages-received'))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
resp = c.get(reverse('api:my-messages-received'))
self.assertEqual(resp.json(), [])
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user1,
timestamp=datetime(2020, 5, 4, 3, 2, 1, tzinfo=UTC),
subject='Test Message2',
body='Message Body2',
)
dm.save()
resp = c.get(reverse('api:my-messages-received'))
self.assertEqual(
resp.json(),
[
{
'id': str(dm.pk),
'sender': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'recipient': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'has_responded': False,
}
],
)
dm.has_responded = True
dm.deleted_by_sender = True
dm.save()
resp = c.get(reverse('api:my-messages-received'))
self.assertEqual(
resp.json(),
[
{
'id': str(dm.pk),
'sender': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'recipient': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'has_responded': True,
}
],
)
dm.deleted_by_recipient = True
dm.save()
resp = c.get(reverse('api:my-messages-received'))
self.assertEqual(resp.json(), [])
def test_DirectMessagesSent(self):
c = Client()
resp = c.get(reverse('api:my-messages-sent'))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
resp = c.get(reverse('api:my-messages-sent'))
self.assertEqual(resp.json(), [])
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user1,
recipient=self.human_user2,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
dm.save()
resp = c.get(reverse('api:my-messages-sent'))
self.assertEqual(
resp.json(),
[
{
'id': str(dm.pk),
'sender': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'recipient': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
}
],
)
dm.deleted_by_recipient = True
dm.save()
resp = c.get(reverse('api:my-messages-sent'))
self.assertEqual(len(resp.json()), 1)
dm.deleted_by_sender = True
dm.save()
resp = c.get(reverse('api:my-messages-sent'))
self.assertEqual(resp.json(), [])
def test_DirectMessageReceived(self):
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user1,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
dm.save()
other_dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user2,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
other_dm.save()
c = Client()
resp = c.get(reverse('api:my-message-received', kwargs={'pk': str(dm.pk)}))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
resp = c.get(reverse('api:my-message-received', kwargs={'pk': str(other_dm.pk)}))
self.assertEqual(resp.status_code, 404)
resp = c.get(reverse('api:my-message-received', kwargs={'pk': str(dm.pk)}))
self.assertEqual(
resp.json(),
{
'id': str(dm.pk),
'sender': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'recipient': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'body': dm.body,
'has_responded': False,
},
)
dm.has_responded = True
dm.deleted_by_sender = True
dm.save()
resp = c.get(reverse('api:my-message-received', kwargs={'pk': str(dm.pk)}))
self.assertEqual(
resp.json(),
{
'id': str(dm.pk),
'sender': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'recipient': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'body': dm.body,
'has_responded': True,
},
)
dm.deleted_by_recipient = True
dm.save()
resp = c.get(reverse('api:my-message-received', kwargs={'pk': str(other_dm.pk)}))
self.assertEqual(resp.status_code, 404)
def test_DirectMessageSent(self):
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user1,
recipient=self.human_user2,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
dm.save()
other_dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user2,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
other_dm.save()
c = Client()
resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(dm.pk)}))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(other_dm.pk)}))
self.assertEqual(resp.status_code, 404)
resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(dm.pk)}))
self.assertEqual(
resp.json(),
{
'id': str(dm.pk),
'sender': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'recipient': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'body': dm.body,
},
)
dm.deleted_by_recipient = True
dm.save()
resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(dm.pk)}))
self.assertEqual(
resp.json(),
{
'id': str(dm.pk),
'sender': {'username': self.human_user1.username, 'display_name': self.human_user1.display_name},
'recipient': {'username': self.human_user2.username, 'display_name': self.human_user2.display_name},
'timestamp': localtime(dm.timestamp).isoformat(),
'in_reply_to': None,
'subject': dm.subject,
'body': dm.body,
},
)
dm.deleted_by_sender = True
dm.save()
resp = c.get(reverse('api:my-message-sent', kwargs={'pk': str(other_dm.pk)}))
self.assertEqual(resp.status_code, 404)
def test_DirectMessagesSend(self):
c = Client()
resp = c.post(reverse('api:my-send-message'))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
with freeze_time(datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC)):
resp = c.post(
reverse('api:my-send-message'),
{
'recipient': self.human_user2.username,
'subject': 'Message Subject',
'body': 'Message Body',
},
)
self.assertEqual(resp.status_code, 201)
self.assertEqual(DirectMessage.objects.all().count(), 1)
dm = DirectMessage.objects.get()
self.assertEqual(dm.conference, self.conference1)
self.assertEqual(dm.sender, self.human_user1)
self.assertEqual(dm.recipient, self.human_user2)
self.assertEqual(dm.timestamp, datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC))
self.assertEqual(dm.subject, 'Message Subject')
self.assertEqual(dm.body, 'Message Body')
self.assertFalse(dm.was_read)
self.assertFalse(dm.has_responded)
self.assertFalse(dm.deleted_by_recipient)
with freeze_time(datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC)):
resp = c.post(
reverse('api:my-send-message'),
{
'recipient': self.human_user2.username,
'subject': 'Message Subject2',
'body': 'Message Body2',
},
)
self.assertEqual(DirectMessage.objects.all().count(), 2)
dm2 = DirectMessage.objects.get(pk=resp.json()['id'])
self.assertEqual(dm2.timestamp, datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC))
self.human_user1.shadow_banned = True
self.human_user1.save()
with freeze_time(datetime(2020, 3, 5, 7, 9, 11, tzinfo=UTC)):
resp = c.post(
reverse('api:my-send-message'),
{
'recipient': self.human_user2.username,
'subject': 'Message Subject, Sender is shadow banned',
'body': 'Message Body, Sender is shadow banned',
},
)
self.assertEqual(DirectMessage.objects.all().count(), 3)
dm3 = DirectMessage.objects.get(pk=resp.json()['id'])
self.assertTrue(dm3.deleted_by_recipient)
def test_DirectMessageDelete(self):
c = Client()
resp = c.delete(reverse('api:my-delete-message', kwargs={'pk': str(uuid.uuid4())}))
self.assertEqual(resp.status_code, 403)
c.force_login(self.human_user1)
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user1,
recipient=self.human_user2,
timestamp=datetime(2020, 5, 4, 3, 2, 1, tzinfo=UTC),
subject='Test Message',
body='Message Body',
)
dm.save()
resp = c.delete(reverse('api:my-delete-message', kwargs={'pk': str(dm.pk)}))
self.assertEqual(resp.status_code, 204)
dm.refresh_from_db()
self.assertTrue(dm.deleted_by_sender)
self.assertFalse(dm.deleted_by_recipient)
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user1,
timestamp=datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC),
subject='Test Message2',
body='Message Body2',
)
dm.save()
resp = c.delete(reverse('api:my-delete-message', kwargs={'pk': str(dm.pk)}))
self.assertEqual(resp.status_code, 204)
dm.refresh_from_db()
self.assertFalse(dm.deleted_by_sender)
self.assertTrue(dm.deleted_by_recipient)
dm = DirectMessage(
conference=self.conference1,
sender=self.human_user2,
recipient=self.human_user1,
timestamp=datetime(2020, 2, 2, 2, 2, 2, tzinfo=UTC),
deleted_by_sender=True,
subject='Test Message3',
body='Message Body3',
)
dm.save()
resp = c.delete(reverse('api:my-delete-message', kwargs={'pk': str(dm.pk)}))
self.assertEqual(resp.status_code, 204)
self.assertEqual(DirectMessage.objects.filter(pk=dm.pk).count(), 0)
......@@ -4,7 +4,7 @@ from rest_framework.urlpatterns import format_suffix_patterns
from django.conf import settings
from django.urls import path
from .views import api_root, assemblies, badges, bbb, conferencemember, conferences, events, maps, metanav, rooms, schedule, users, workadventure
from .views import api_root, assemblies, badges, bbb, conferencemember, conferences, events, maps, messages, metanav, rooms, schedule, users, workadventure
app_name = 'api'
urlpatterns = [
......@@ -13,6 +13,14 @@ urlpatterns = [
path('me', users.profile, name='profile'),
path('me/badges.zip', users.BadgeExportView.as_view(), name='badge-export'),
path('me/badges', users.badges, name='badges'),
path('me/events', events.EventMyFavorites.as_view(), name='my-events'),
path('me/events/<uuid:pk>', events.EventMyFavoritesSet.as_view(), name='my-events-set'),
path('me/received-messages/', messages.DirectMessagesReceived.as_view(), name='my-messages-received'),
path('me/sent-messages/', messages.DirectMessagesSent.as_view(), name='my-messages-sent'),
path('me/received-messages/<uuid:pk>', messages.DirectMessageReceived.as_view(), name='my-message-received'),
path('me/sent-messages/<uuid:pk>', messages.DirectMessageSent.as_view(), name='my-message-sent'),
path('me/send-message', messages.DirectMessageSend.as_view(), name='my-send-message'),
path('me/delete-message/<uuid:pk>', messages.DirectMessageDelete.as_view(), name='my-delete-message'),
path('me/friends', users.friends, name='friends'),
path('me/timeline', users.UserTimelineList.as_view(), name='timeline-list'),
# conference-specific views
......
from rest_framework import generics
from rest_framework import generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
......@@ -21,3 +23,22 @@ class EventDetail(ConferenceSlugMixin, generics.RetrieveAPIView):
def get_object(self, **kwargs):
event_id = self.request.resolver_match.kwargs['pk']
return get_object_or_404(Event.objects.conference_accessible(conference=self.conference), pk=event_id)
class EventMyFavorites(ConferenceSlugMixin, generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated]
def list(self, request, *args, **kwargs):
return Response(self.request.user.favorite_events.values_list('pk', flat=True))
class EventMyFavoritesSet(ConferenceSlugMixin, APIView):
permission_classes = [permissions.IsAuthenticated]
def put(self, request, *args, pk=None, **kwargs):
self.request.user.favorite_events.add(*Event.objects.conference_accessible(conference=self.conference).filter(pk=pk))
return Response(True)
def delete(self, request, *args, pk=None, **kwargs):
self.request.user.favorite_events.remove(*Event.objects.conference_accessible(conference=self.conference).filter(pk=pk))
return Response(True)
from rest_framework import generics, permissions
from django.db.models import Q
from core.models.messages import DirectMessage
from ..serializers import (
DirectMessageSendSerializer,
DirectMessageSerializerReceived,
DirectMessageSerializerReceivedShort,
DirectMessageSerializerSent,
DirectMessageSerializerSentShort,
)
from .mixins import ConferenceSlugMixin
class DirectMessagesReceived(ConferenceSlugMixin, generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = DirectMessageSerializerReceivedShort
def get_queryset(self):
return (
DirectMessage.objects.filter(conference=self.conference, recipient=self.request.user, deleted_by_recipient=False)
.select_related('recipient', 'sender')
.order_by('-timestamp')
)
class DirectMessagesSent(ConferenceSlugMixin, generics.ListAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = DirectMessageSerializerSentShort
def get_queryset(self):
return (
DirectMessage.objects.filter(conference=self.conference, sender=self.request.user, deleted_by_sender=False)
.select_related('recipient', 'sender')
.order_by('-timestamp')
)
class DirectMessageReceived(ConferenceSlugMixin, generics.RetrieveUpdateAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = DirectMessageSerializerReceived
def get_queryset(self):
return (
DirectMessage.objects.filter(conference=self.conference, recipient=self.request.user, deleted_by_recipient=False)
.select_related('recipient', 'sender')
.order_by('-timestamp')
)
class DirectMessageSent(ConferenceSlugMixin, generics.RetrieveAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = DirectMessageSerializerSent
def get_queryset(self):
return (
DirectMessage.objects.filter(conference=self.conference, sender=self.request.user, deleted_by_sender=False)
.select_related('recipient', 'sender')
.order_by('-timestamp')
)
class DirectMessageSend(ConferenceSlugMixin, generics.CreateAPIView):
permission_classes = [permissions.IsAuthenticated]
serializer_class = DirectMessageSendSerializer
def perform_create(self, serializer: DirectMessageSendSerializer):
serializer.save(
conference=self.conference,
sender=self.request.user,
deleted_by_recipient=self.request.user != serializer.validated_data['recipient'] and self.request.user.shadow_banned,
)
class DirectMessageDelete(ConferenceSlugMixin, generics.DestroyAPIView):
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return DirectMessage.objects.filter(
Q(conference=self.conference, recipient=self.request.user, deleted_by_recipient=False)
| Q(conference=self.conference, sender=self.request.user, deleted_by_sender=False)
)
def perform_destroy(self, instance: DirectMessage):
if instance.sender == self.request.user:
if instance.deleted_by_recipient:
instance.delete()
return
instance.deleted_by_sender = True
instance.save(update_fields=['deleted_by_sender'])
elif instance.recipient == self.request.user:
if instance.deleted_by_sender:
instance.delete()
return
instance.deleted_by_recipient = True
instance.save(update_fields=['deleted_by_recipient'])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment