pythondjangodjango-modelsdjango-rest-frameworkdjango-views

How to pass a related objects parameters in the main objects serializer


I have a Room object, which will have some settings. I wanted to make Room and RoomSettings different objects but bound to each other. RoomSettings will be created whenever a Room is created, so I'm using signals. However, while creating the room, I need to pass RoomSettings arguments from Room to RoomSettings through signals, so I can set the settings in creation.

models.py

from django.db import models
from users.models import AppUser as UserModel
from django.core.validators import MaxValueValidator, MinValueValidator

import random, string
from datetime import datetime

def generate_unique_key():
    length = 10
    while True:
        key = ''.join(random.choices(string.ascii_uppercase, k=length))
        if Room.objects.filter(key=key).count() == 0:
            break
    return key

class Room(models.Model):
    host = models.ForeignKey(UserModel, on_delete=models.CASCADE, related_name='rooms')
    key = models.CharField(max_length=10, default=generate_unique_key, unique=True, editable=False)
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    
    def __str__(self):
        return f'{self.key} - {self.host.username}'
    
    def get_chat(self):
        return self.chat

class RoomSettings(models.Model):
    room = models.OneToOneField(Room, on_delete=models.CASCADE, related_name='settings')
    max_users = models.PositiveIntegerField(default=8, validators=[MaxValueValidator(8), MinValueValidator(1)])
    is_public = models.BooleanField(default=True)
    users_can_send_message = models.BooleanField(default=True)

serializers.py

from rest_framework import serializers

from .models import Room, RoomSettings
from users.serializers import UserSerializer
from chats.serializers import RoomChatSerializer


class SettingSerializer(serializers.ModelSerializer):
    class Meta:
        model = RoomSettings
        exclude = ['room']

        
class CreateRoomSerializer(serializers.ModelSerializer):
    max_users = serializers.IntegerField()
    is_public = serializers.BooleanField()
    users_can_send_message = serializers.BooleanField()
    class Meta:
        model = Room
        fields = ['max_users', 'is_public', 'users_can_send_message']

signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Room, RoomSettings
from chats.models import Chat

@receiver(post_save, sender=Room)
def post_save_create_room(sender, instance, created, **kwargs):
    if created:
        RoomSettings.objects.create(room=instance, **kwargs)
        Chat.objects.create(room=instance)

views.py

class CreateRoomView(CreateAPIView):
    permission_classes = [IsAuthenticated]
    serializer_class = CreateRoomSerializer
    
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            room = Room.objects.create(host=self.request.user, **serializer.data)
            return JsonResponse(RoomSerializer(room).data, status=status.HTTP_201_CREATED)

I tried to pass arguments to the endpoint for CreateRoomView, but got errors. It should create Room and RoomSettings with given data.


Solution

  • I don't really see why you need a RoomSettings model in the first place, especially since you each time create a RoomSettings record for a Room. The purpose of a OneToOneField is to make the extra model (here RoomSettings) optional, but here you seem to "juggle" with two models where there is no use-case to split these in two.

    Just join these together in one model:

    from django.conf import settings
    
    def generate_unique_key():
        length = 10
        while True:
            key = ''.join(random.choices(string.ascii_uppercase, k=length))
            if not Room.objects.filter(key=key).exists():
                break
        return key
    
    
    class Room(models.Model):
        host = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            on_delete=models.CASCADE,
            related_name='rooms',
            editable=False,
        )
        key = models.CharField(
            max_length=10, default=generate_unique_key, unique=True, editable=False
        )
        created_at = models.DateTimeField(auto_now_add=True)
        max_users = models.PositiveIntegerField(
            default=8, validators=[MaxValueValidator(8), MinValueValidator(1)]
        )
        is_public = models.BooleanField(default=True)
        users_can_send_message = models.BooleanField(default=True)
    
        def __str__(self):
            return f'{self.key} - {self.host.username}'
    
        def get_chat(self):
            return self.chat

    This simplifies the serializer to:

    class RoomSerializer(serializers.ModelSerializer):
        max_users = serializers.IntegerField()
        is_public = serializers.BooleanField()
        users_can_send_message = serializers.BooleanField()
    
        class Meta:
            model = Room
            fields = ['max_users', 'is_public', 'users_can_send_message']

    and thus creating the object to:

    class CreateRoomView(CreateAPIView):
        permission_classes = [IsAuthenticated]
        serializer_class = RoomSerializer
    
        def perform_create(self, serializer):
            serializer.save(host=request.user)

    Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation [Django-doc].


    Note: It is better to work with .exists() [Django-doc] to check if a record exists than to work with .count() [Django-doc] since counting might have to keep looking for records after finding one.