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!
- AI Project from Scratch, The Idea, Alive Diary
- Prove it is feasible with Google AI Studio
- Django API Project Setup
- Django accounts management (1), registration and activation
- Django accounts management (2), login and change password
- Django Rest framework with Swagger
- Django accounts management (3), forgot password and account details
- 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 *
app_main/serializers.py
and the urls file
from django.urls import path, include
from .views import *
urlpatterns = [
]
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"),
]
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
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
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',
]
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")
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
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),
]
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)
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.
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']
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)
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()),
]
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
and we are done! we will cover the AI integration in our next tutorial so
Stay tuned 😎
Top comments (0)