DEV Community

Cover image for Diary App, diaries and messaging APIs
Saad Alkentar
Saad Alkentar

Posted on

Diary App, diaries and messaging APIs

What to expect from this article?

We finished building the account management app on previous articles, this article will concentrate on the main diary Django app. It will cover

  • Models for diaries, conversations, and messages.
  • diaries and messages Serializer.
  • Views for the diaries and messages APIs, but without AI integration.
  • And of course the URLs.

I'll try to cover as many details as possible without boring you, but I still expect you to be familiar with some aspects of Python and Django.

the final version of the source code can be found at https://github.com/saad4software/alive-diary-backend

Series order

Check previous articles if interested!

  1. AI Project from Scratch, The Idea, Alive Diary
  2. Prove it is feasible with Google AI Studio
  3. Django API Project Setup
  4. Django accounts management (1), registration and activation
  5. Django accounts management (2), login and change password
  6. Django Rest framework with Swagger
  7. Django accounts management (3), forgot password and account details
  8. Diary App, diaries API (You are here 📍)

Main app setup

let's create serializers file in the app

from rest_framework import serializers
from app_main.models import *
Enter fullscreen mode Exit fullscreen mode

app_main/serializers.py

and the urls file

from django.urls import path, include
from .views import *

urlpatterns = [

]
Enter fullscreen mode Exit fullscreen mode

app_main/urls.py

finally, let's connect the app urls to the project urls by editing projects urls file as

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/account/', include('app_account.urls')),
    path('api/', include('app_main.urls')), #new

    path('docs/', include_docs_urls(title=API_TITLE,description=API_DESCRIPTION)),
    path('swagger/', schema_view.with_ui('swagger',cache_timeout=0),name="swagger-schema"),
]
Enter fullscreen mode Exit fullscreen mode

alive_diary/urls.py

Now we can call any url with the prefix "api/"

The Models

We need diary model, every diary belongs to a user, and have conversations inside it, each conversation could have multiple messages. the models should look like this

from django.db import models
from app_account.models import User


class Diary(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)

    user = models.ForeignKey(User, to_field='id', on_delete=models.CASCADE)
    readers = models.ManyToManyField(User, related_name="diaries", blank=True)

    title = models.CharField(max_length=255, null=True, blank=True)
    is_memory = models.BooleanField(default=False)

    def __str__(self):
        return self.user.username

class Conversation(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)
    diary = models.ForeignKey(Diary, related_name="conversations",  to_field='id', on_delete=models.CASCADE)

    def __str__(self):
        return self.diary.title


class Message(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)

    conversation = models.ForeignKey(Conversation, related_name="messages", to_field='id', on_delete=models.CASCADE)

    text = models.TextField(null=True, blank=True)
    is_user = models.BooleanField(default=True)

    def __str__(self):
        return self.text
Enter fullscreen mode Exit fullscreen mode

app_main/models.py

each diary belongs to one user, but can be shared with multiple readers, so there is one user and many-to-many readers. it has a title, instead of just calling it Alex's diary 😁. and due to the similarity between diaries and memories as discussed in Prove it is feasible with Google AI Studio, we added a simple boolean key to the model to state weather it is a diary or a memory.

conversations are related to diaries, they are basically a way to group messages, like folders containing files.

messages are grouped in conversations, and they are either written by the user or by the AI module, that is why we added is_user key to distinguish the writer.

let's create the models using django migrations

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

The diaries API

As always, let's start with the serializer

from rest_framework import serializers
from app_main.models import *

class DiarySerializer(serializers.ModelSerializer):
    first_name = serializers.CharField(source='user.first_name', read_only=True)
    last_name = serializers.CharField(source='user.last_name', read_only=True)


    class Meta:
        model = Diary
        fields = [
            'created',
            'active',
            'first_name',
            'last_name',
            'title',
            'is_memory',
            'id'
        ]
        read_only_fields = [
            'id', 
            'is_memory', 
            'active', 
        ]
Enter fullscreen mode Exit fullscreen mode

app_main/serializers.py

We are using model serializer and listing what fields to serialize. we have added two extra read only fields from user model, the first name and last name.

let's move to the views file

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from .serializers import *
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from common.utils import CustomRenderer, StandardResultsSetPagination
import rest_framework.filters
from django_filters import rest_framework as filters

class DiaryViewSet(ModelViewSet):
    permission_classes = (IsAuthenticated, )
    lookup_field = 'id'
    serializer_class = DiarySerializer
    pagination_class = StandardResultsSetPagination
    renderer_classes = [CustomRenderer, BrowsableAPIRenderer]
    filter_backends = [filters.DjangoFilterBackend, rest_framework.filters.SearchFilter]
    search_fields = ['title']

    def get_queryset(self):
        return Diary.objects.filter(user=self.request.user).order_by("-created")

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response("success")
Enter fullscreen mode Exit fullscreen mode

app_main/views.py

We are creating a viewset to simplify the CRUD operations, this api is for authenticated users only, so we set permission_classes to IsAuthenticated, we are using our diary serializer as serializing schema for this model, we have already covered the custom renderer in Django accounts management (1), registration and activation, in short, it is used to control the response schema of the whole app. we have added pagination_class to control page size parameter value, it looks like

from rest_framework.pagination import PageNumberPagination

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000
Enter fullscreen mode Exit fullscreen mode

common/utils.py

we have overwritten the get_queryset to query only this users diary. overwritten perform_create to set current user as owner. and overwritten the destroy to return a message instead of the silence 204 deleted successfully code!

By explicitly binding the viewset URLs as explained here, the URLs file should look like

from django.urls import path, include
from .views import *

diaries_list = DiaryViewSet.as_view({
    'get': 'list',
    'post': 'create'
})

diaries_details = DiaryViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})

urlpatterns = [
    path('diaries/', diaries_list),
    path('diaries/<int:id>/', diaries_details),
]
Enter fullscreen mode Exit fullscreen mode

app_main/urls.py

we should be able to test this api directly on swagger (swagger implementation is explained in Django Rest framework with Swagger)

Create diary

after logging in, and authenticating using Bearer JWT (as explained in swagger article). we should be able to create a diary, using diaries_create and list diaries using diaries_list from swagger. all CRUD functions should be available.

Diaries CRUD

Messaging API

Let's think about this first, we have conversations and messages. Normally, every time the user try to talk to the AI, it creates a conversation for this talk. But for a diary, I would like to keep a single conversation per day. so, we will create a custom conversation API with

  • a GET request, that can find (or create) the account main diary item, this day conversation, and start a worm welcome from the AI model.
  • a POST request, with the user message and conversation and diary ids. I know, it looks unsafe 😵‍💫, if we are getting the ids from the user and not verifying their ownership, virtually, users can sniff others diaries, mmmm 😓. So we need to make sure the diary id and conversation id belongs to this user!

it is a conflict between database efficiency (hitting the database with each message to check ownership) and security, let's vote for security this time, and make sure the ids belong to the current user.

so, let's start with the message serializer

class MessageSerializer(serializers.ModelSerializer):

    class Meta:
        model = Message
        fields = [
            'created',
            'active',

            'text',
            'is_user',
            'conversation',

            'id'
        ]
        read_only_fields = ['id']
Enter fullscreen mode Exit fullscreen mode

app_main/serializers.py

it is a model serializer. we listed all fields to serialize. now moving to the views

from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
import datetime
from rest_framework.response import Response
from rest_framework.exceptions import APIException

class ConversationsView(APIView):
    permission_classes = (IsAuthenticated, )
    renderer_classes = [CustomRenderer, BrowsableAPIRenderer]

    def get_conversation(self):
        diary = Diary.objects.filter(
                    user=self.request.user,
                    is_memory=False,
                ).first()

        if not diary: 
            diary = Diary(
                user=self.request.user, 
                is_memory=False,
                title="My diary",
            )
            diary.save()

        conversation = Conversation.objects.filter(
            diary=diary,
            created__date=datetime.date.today(),
        ).first()

        if not conversation:
            conversation = Conversation(
                diary=diary,
            )
            conversation.save()

        return conversation

    def get(self, request, **kwargs):

        conversation = self.get_conversation()
        ai_response = "hello world" # get ai response

        ai_message = Message(
            text=ai_response, 
            conversation=conversation,
            is_user=False
        )
        ai_message.save()

        return Response(MessageSerializer(ai_message).data)


    @swagger_auto_schema(request_body=MessageSerializer)
    def post(self, request, **kwargs):

        serializer = MessageSerializer(data=request.data)
        if not serializer.is_valid():
            raise APIException(serializer.errors)

        conversation = self.get_conversation()

        message = serializer.save(
            conversation=conversation,
            is_user=True,
        ) # save users message

        ai_response = "hello world again" # get ai response
        ai_message = Message(
            text=ai_response, 
            conversation=conversation,
            is_user=False
        ) # save ai response message
        ai_message.save()

        return Response(MessageSerializer(ai_message).data)

Enter fullscreen mode Exit fullscreen mode

app_main/views.py

We have created a get_conversation function, designed to get today's conversation if exists, or create it if not. It was used in both GET and POST requests to make sure users own the conversation and diary they are using. We left the AI connection to the next article to give it enough space.
Let's add this view to the URLs file

urlpatterns = [
    path('diaries/', diaries_list),
    path('diaries/<int:id>/', diaries_details),
    path('conversation/', ConversationsView.as_view()),
]
Enter fullscreen mode Exit fullscreen mode

app_main/urls.py

nice and easy, let's test our new API on swagger, after logging in and setting the Bearer token, we can test the get request directly

Messages API

and we are done! we will cover the AI integration in our next tutorial so

Stay tuned 😎

Top comments (0)