drf-directmessages 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: drf-directmessages
3
+ Version: 0.0.1
4
+ Summary: An API allowing users to directly message each other.
5
+ Author: Garreth Cain
6
+ Author-email: garrethccain@gmail.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Requires-Dist: django (>=4.2.1,<5.0.0)
13
+ Requires-Dist: djangorestframework (>=3.14.0,<4.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # DirectMessages
17
+
18
+ A small, lightweight and easy to use Rest endpoint to add messaging between
19
+ your users.
20
+
@@ -0,0 +1,4 @@
1
+ # DirectMessages
2
+
3
+ A small, lightweight and easy to use Rest endpoint to add messaging between
4
+ your users.
@@ -0,0 +1,11 @@
1
+ __version__ = '0.9.7'
2
+
3
+ try:
4
+ from django import VERSION as DJANGO_VERSION
5
+ if DJANGO_VERSION >= (1, 7):
6
+ default_app_config = 'directmessages.apps.DirectmessagesConfig'
7
+ else:
8
+ from directmessages.apps import populateInbox
9
+ populateInbox()
10
+ except ImportError:
11
+ pass
@@ -0,0 +1,8 @@
1
+ from .models import Message
2
+ from django.contrib import admin
3
+
4
+ class MessageAdmin(admin.ModelAdmin):
5
+ model = Message
6
+ list_display = ('id', 'sender', 'content', )
7
+
8
+ admin.site.register(Message, MessageAdmin)
@@ -0,0 +1,14 @@
1
+ from django.apps import AppConfig
2
+
3
+ Inbox = None
4
+
5
+
6
+ class AppConfig(AppConfig):
7
+ default_auto_field = "django.db.models.BigAutoField"
8
+ name = "directmessages"
9
+
10
+ def ready(self):
11
+ # For convenience
12
+ from .services import MessagingService
13
+ global Inbox
14
+ Inbox = MessagingService()
@@ -0,0 +1,58 @@
1
+ # Generated by Django 4.2.1 on 2023-05-13 08:24
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="Message",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ("content", models.TextField(verbose_name="Content")),
30
+ (
31
+ "sent_at",
32
+ models.DateTimeField(auto_now=True, verbose_name="sent at"),
33
+ ),
34
+ (
35
+ "read_at",
36
+ models.DateTimeField(blank=True, null=True, verbose_name="read at"),
37
+ ),
38
+ (
39
+ "recipient",
40
+ models.ForeignKey(
41
+ on_delete=django.db.models.deletion.CASCADE,
42
+ related_name="received_dm",
43
+ to=settings.AUTH_USER_MODEL,
44
+ verbose_name="Recipient",
45
+ ),
46
+ ),
47
+ (
48
+ "sender",
49
+ models.ForeignKey(
50
+ on_delete=django.db.models.deletion.CASCADE,
51
+ related_name="sent_dm",
52
+ to=settings.AUTH_USER_MODEL,
53
+ verbose_name="Sender",
54
+ ),
55
+ ),
56
+ ],
57
+ ),
58
+ ]
@@ -0,0 +1,44 @@
1
+ from django.db import models
2
+ from django.utils import timezone
3
+ from django.contrib.auth import get_user_model
4
+ from django.core.exceptions import ValidationError
5
+
6
+
7
+ # from django.contrib.auth.models import User
8
+
9
+ User = get_user_model()
10
+
11
+
12
+ class Message(models.Model):
13
+ """
14
+ A private directmessage
15
+ """
16
+ content = models.TextField('Content')
17
+ sender = models.ForeignKey(User,
18
+ related_name='sent_dm',
19
+ verbose_name="Sender",
20
+ on_delete=models.CASCADE)
21
+ recipient = models.ForeignKey(User,
22
+ related_name='received_dm',
23
+ verbose_name="Recipient",
24
+ on_delete=models.CASCADE)
25
+ sent_at = models.DateTimeField("sent at", auto_now=True)
26
+ read_at = models.DateTimeField("read at", null=True, blank=True)
27
+
28
+ @property
29
+ def unread(self):
30
+ """
31
+ returns whether the message was read or not
32
+ """
33
+ return self.read_at is None
34
+
35
+ def __str__(self):
36
+ return f"{self.id}/{self.sender}/{self.recipient}/{self.content}"
37
+
38
+ def save(self, **kwargs):
39
+ if self.sender == self.recipient:
40
+ raise ValidationError("You can't send messages to yourself")
41
+
42
+ if not self.id:
43
+ self.sent_at = timezone.now()
44
+ super(Message, self).save(**kwargs)
@@ -0,0 +1,85 @@
1
+ from django.contrib.auth import get_user_model
2
+
3
+ from rest_framework import serializers
4
+
5
+ from .models import Message
6
+
7
+
8
+ User = get_user_model()
9
+
10
+
11
+ class UnreadMessageSerializer(serializers.ModelSerializer):
12
+ count = serializers.SerializerMethodField()
13
+
14
+ def get_count(self, obj):
15
+ breakpoint()
16
+ return Message.objects.filter(read_at=None, recipient=obj).count()
17
+
18
+ class Meta:
19
+ model = User
20
+ fields = (
21
+ "id",
22
+ "count",
23
+ )
24
+
25
+
26
+ class ConversationSerializer(serializers.ModelSerializer):
27
+
28
+ class Meta:
29
+ model = User
30
+ fields = (
31
+ "id",
32
+ "email",
33
+ "first_name",
34
+ "last_name",
35
+ "is_active",
36
+ "date_joined",
37
+ )
38
+
39
+
40
+ class MessageSerializer(serializers.ModelSerializer):
41
+ direction = serializers.SerializerMethodField()
42
+
43
+ def get_direction(self, obj):
44
+ request = self.context.get("request")
45
+ user_id = request.user if request else None
46
+ if type(obj) != Message:
47
+ return ""
48
+ return (
49
+ "in" if user_id == obj.recipient
50
+ else
51
+ "out"
52
+ )
53
+
54
+ class Meta:
55
+ model = Message
56
+ fields = (
57
+ "id",
58
+ "sender",
59
+ "recipient",
60
+ "direction",
61
+ "sent_at",
62
+ "read_at",
63
+ "content",
64
+ )
65
+ read_only_fields = (
66
+ "sender",
67
+ "recipient",
68
+ "sent_at",
69
+ "read_at",
70
+ )
71
+
72
+
73
+ class MessageSendSerializer(serializers.ModelSerializer):
74
+
75
+ class Meta:
76
+ model = Message
77
+ fields = (
78
+ "id",
79
+ "content",
80
+ )
81
+ read_only_fields = (
82
+ " sender",
83
+ "sent_at",
84
+ "read_at",
85
+ )
@@ -0,0 +1,152 @@
1
+ from __future__ import unicode_literals
2
+
3
+ from .models import Message
4
+ from .signals import message_read, message_sent
5
+ from django.core.exceptions import ValidationError
6
+ from django.utils import timezone
7
+ from django.db.models import Q
8
+
9
+
10
+ class MessagingService(object):
11
+ """
12
+ A object to manage all messages and conversations
13
+ """
14
+
15
+ # Message creation
16
+
17
+ def send_message(self, sender, recipient, message):
18
+ """
19
+ Send a new message
20
+ :param sender: user
21
+ :param recipient: user
22
+ :param message: String
23
+ :return: Message and status code
24
+ """
25
+
26
+ if sender == recipient:
27
+ raise ValidationError("You can't send messages to yourself.")
28
+
29
+ message = Message(
30
+ sender=sender,
31
+ recipient=recipient,
32
+ content=str(message))
33
+ message.save()
34
+
35
+ message_sent.send(sender=message,
36
+ from_user=message.sender,
37
+ to=message.recipient)
38
+
39
+ # The second value acts as a status value
40
+ return message, 200
41
+
42
+ # Message reading
43
+ def get_unread_messages(self, user):
44
+ """
45
+ List of unread messages for a specific user
46
+ :param user: user
47
+ :return: messages
48
+ """
49
+ return Message.objects.all().filter(recipient=user,
50
+ read_at=None).distinct()
51
+
52
+ # Message reading
53
+ def get_unread_message_count(self, user):
54
+ """
55
+ Count of unread messages for a specific user
56
+ :param user: user
57
+ :return: messages
58
+ """
59
+ return Message.objects.all().filter(recipient=user,
60
+ read_at=None).count()
61
+
62
+ def read_message(self, message_id):
63
+ """
64
+ Read specific message
65
+ :param message_id: Integer
66
+ :return: Message Text
67
+ """
68
+ try:
69
+ message = Message.objects.get(id=message_id)
70
+ self.mark_as_read(message)
71
+ return message.content
72
+ except Message.DoesNotExist:
73
+ return ""
74
+
75
+ def read_message_formatted(self, message_id):
76
+ """
77
+ Read a message in the format <User>: <Message>
78
+ :param message_id: Id
79
+ :return: Formatted Message Text
80
+ """
81
+ try:
82
+ message = Message.objects.get(id=message_id)
83
+ self.mark_as_read(message)
84
+ return f"{message.sender.email}: {message.content}"
85
+ except Message.DoesNotExist:
86
+ return ""
87
+
88
+ ######
89
+ # Conversation management
90
+ ######
91
+ def get_conversations(self, user):
92
+ """
93
+ Lists all conversation-partners for a specific user
94
+ :param user: User
95
+ :return: Conversation list
96
+ """
97
+ all_conversations = Message.objects.all().filter(
98
+ Q(sender=user) | Q(recipient=user)
99
+ )
100
+
101
+ contacts = []
102
+ for conversation in all_conversations:
103
+ if conversation.sender != user:
104
+ contacts.append(conversation.sender)
105
+ elif conversation.recipient != user:
106
+ contacts.append(conversation.recipient)
107
+
108
+ # To abolish duplicates
109
+ return list(set(contacts))
110
+
111
+ def get_conversation(self, user1, user2, limit=None,
112
+ reversed=False, mark_read=False):
113
+ """
114
+ List of messages between two users
115
+ :param user1: User
116
+ :param user2: User
117
+ :param limit: int
118
+ :param reversed: Boolean - Makes the newest message be at index 0
119
+ :return: messages
120
+ """
121
+ users = [user1, user2]
122
+
123
+ # Newest message first if it's reversed (index 0)
124
+ order = "-pk" if reversed else "pk"
125
+ conversation = Message.objects.all().filter(
126
+ sender__in=users,
127
+ recipient__in=users).order_by(order)
128
+
129
+ if limit:
130
+ # Limit number of messages to the x newest
131
+ conversation = conversation[:limit]
132
+
133
+ if mark_read:
134
+ for message in conversation:
135
+ # Just to be sure, everything is read
136
+ self.mark_as_read(message)
137
+
138
+ return conversation
139
+
140
+ # Helper methods
141
+ def mark_as_read(self, message):
142
+ """
143
+ Marks a message as read, if it hasn't been read before
144
+ :param message: Message
145
+ """
146
+
147
+ if message.read_at is None:
148
+ message.read_at = timezone.now()
149
+ message_read.send(sender=message,
150
+ from_user=message.sender,
151
+ to=message.recipient)
152
+ message.save()
@@ -0,0 +1,4 @@
1
+ from django.dispatch import Signal
2
+
3
+ message_sent = Signal() # Signal(providing_args=['from_user', 'to'])
4
+ message_read = Signal() # Signal(providing_args=['from_user', 'to'])
@@ -0,0 +1,15 @@
1
+ from django.urls import path
2
+ from .views import (
3
+ ConversationListView,
4
+ MessageListView,
5
+ UnreadMessagesView,
6
+ MessageSendView
7
+ )
8
+
9
+
10
+ urlpatterns = [
11
+ path("unread/", view=UnreadMessagesView.as_view()),
12
+ path("conversations/", view=ConversationListView.as_view()),
13
+ path("conversations/<int:pk>", view=MessageListView.as_view()),
14
+ path("send/<int:pk>/", view=MessageSendView.as_view()),
15
+ ]
@@ -0,0 +1,85 @@
1
+
2
+ from django.core.exceptions import ValidationError
3
+ from django.contrib.auth import get_user_model
4
+
5
+ from rest_framework import (
6
+ generics,
7
+ status,
8
+ views)
9
+ from rest_framework.response import Response
10
+ from rest_framework.permissions import IsAuthenticated
11
+
12
+ from .apps import Inbox
13
+ from .models import Message
14
+ from .serializers import (
15
+ ConversationSerializer,
16
+ MessageSendSerializer,
17
+ MessageSerializer,
18
+ UnreadMessageSerializer)
19
+ from .services import MessagingService
20
+
21
+
22
+ User = get_user_model()
23
+
24
+
25
+ class MessageViewBase:
26
+ permission_classes = [IsAuthenticated,]
27
+
28
+ def get_user(self):
29
+ return self.request.user
30
+
31
+ def get_recipient(self):
32
+ return User.objects.get(id=self.kwargs['pk'])
33
+
34
+
35
+ class UnreadMessagesView(MessageViewBase, views.APIView):
36
+ serializer_class = UnreadMessageSerializer
37
+
38
+ def get(self, request):
39
+ user = self.get_user()
40
+ serializer = UnreadMessageSerializer(user)
41
+ return Response(data=serializer.data)
42
+
43
+
44
+ class ConversationListView(MessageViewBase, generics.ListAPIView):
45
+ serializer_class = ConversationSerializer
46
+
47
+ def get_queryset(self):
48
+ return Inbox.get_conversations(self.get_user())
49
+
50
+
51
+ class MessageListView(MessageViewBase, generics.ListCreateAPIView):
52
+ serializer_class = MessageSerializer
53
+
54
+ def get_queryset(self):
55
+ user1 = self.get_user()
56
+ user2 = self.kwargs['pk']
57
+ return Inbox.get_conversation(
58
+ user1=user1, user2=user2, mark_read=True).order_by('-sent_at')
59
+
60
+ def create(self, request, *args, **kwargs):
61
+ sender = self.get_user()
62
+ recipient = self.get_recipient()
63
+ content = request.data.get('content')
64
+ _ = Message.objects.create(
65
+ sender=sender, recipient=recipient, content=content)
66
+ return Response(MessageSerializer(
67
+ self.get_queryset(),
68
+ many=True).data, status=status.HTTP_201_CREATED)
69
+
70
+
71
+ class MessageSendView(MessageViewBase, generics.CreateAPIView):
72
+ serializer_class = MessageSendSerializer
73
+
74
+ def create(self, request, *args, **kwargs):
75
+ sender = self.get_user()
76
+ recipient = self.get_recipient()
77
+ content = request.data.get('content')
78
+
79
+ ms = MessagingService()
80
+ try:
81
+ ms.send_message(
82
+ sender=sender, recipient=recipient, message=content)
83
+ except ValidationError as e:
84
+ return Response(e, status=status.HTTP_400_BAD_REQUEST)
85
+ return Response(status=status.HTTP_201_CREATED)
@@ -0,0 +1,21 @@
1
+ [tool.poetry]
2
+ name = "drf-directmessages"
3
+ version = "0.0.1"
4
+ description = "An API allowing users to directly message each other."
5
+ authors = ["Garreth Cain <garrethccain@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "directmessages"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.9"
11
+ django = "^4.2.1"
12
+ djangorestframework = "^3.14.0"
13
+
14
+
15
+ [tool.poetry.group.test.dependencies]
16
+ pytest = "^7.3.1"
17
+ requests-mock = "^1.10.0"
18
+
19
+ [build-system]
20
+ requires = ["poetry-core"]
21
+ build-backend = "poetry.core.masonry.api"