33

I am using Django as the backend server and Vue.js for the front end Movie app.

I have a Ticket model

class MovieTicket(models.Model):
    show = models.ForeignKey(Show)
    seat = models.ForeignKey(Seat)
    user = models.ForeignKey(User)
    purchased_at = models.DateTimeField(default=timezone.now)
    qrcode = models.ImageField(upload_to='qrcode', blank=True, null=True)
    qrcode_data = models.CharField(max_length=255, unique=True, blank=True)

    class Meta:
        unique_together = ('show', 'seat')

And its related Serializer

class MovieTicketSerializer(serializers.ModelSerializer):
    class Meta:
        model = MovieTicket
        fields = '__all__'

To buy a new Ticket there's a view which is mapped to this url http://dev.site.com/api/movies/buy-ticket/:

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def buy_ticket(request):
    serialized = MovieTicketSerializer(data=request.data)
    if serialized.is_valid():
        serialized.save()
        return Response(serialized.data, status=status.HTTP_201_CREATED)
    return Response(serialized._errors, status=status.HTTP_400_BAD_REQUEST)

Now from the front end (Vue.js) I can create a new movie ticket:

const formBody = {
    show: this.$store.state.showSelected.showTime.id,
    user: this.$store.state.user.id,

    // selectedSeats is an array of seats that have been selected by the user. Here I am passing the first seat object.
    seat: this.$store.state.selectedSeats[0].seat.id
};

this.$http.post("http://dev.site.com/api/movies/buy-ticket/", formBody)
    .then(function (response) {
        console.log(response.data);
    })
    .catch(function (response) {
        console.log(response);
    });
return;

If the form was valid, this will create a new MovieTicket Object, or else show the error/s.

Now, suppose if the user selected multiple seats, I can loop through each selectedSeats array and get the seat ids on the client side. And post something like this:

{
    "purchased_at": null,
    "qrcode": null,
    "qrcode_data": "",
    "show": 11,
    "seat": [
        106,
        219
    ],
    "user": 34
}

But what I am confused is how can I pass multiple seat.id if Django rest framework is only accepting one seat per request and display errors accordingly? Meaning display errors if a ticket is available or not, and if its available create movie tickets for that show-seat.

Aamu
  • 3,431
  • 7
  • 39
  • 61

5 Answers5

35

Init the serializer with many=True

In your implementation this is really easy to accomplish:

serialized = MovieTicketSerializer(data=request.data, many=True)

Data is no single object but an array of objects.

Your infos suggest that you need to transform request.data to make those multiple objects (all the same data just different seat number). Right?

anyways:

see: How do I create multiple model instances with Django Rest Framework?

EDIT:

here the info in the drf docu: http://www.django-rest-framework.org/api-guide/serializers/#dealing-with-multiple-objects

(highly suggest to read the drf docs from top to bottom and just playing around with it, before coding your first real implementation. there are many ways to use drf, and knowing all of them leads to better decisions)

EDIT 2 (after question update):

You could send this JSON from the client (see below), or create this format from the current JSON the client sends in your buy_ticket(request) method before you call MovieTicketSerializer(...,many=True):

[
    {
        "purchased_at": null,
        "qrcode": null,
        "qrcode_data": "",
        "show": 11,
        "seat": 106,
        "user": 34
    },
    {
        "purchased_at": null,
        "qrcode": null,
        "qrcode_data": "",
        "show": 11,
        "seat": 219,
        "user": 34
    }
]
Community
  • 1
  • 1
fetzig
  • 1,642
  • 1
  • 13
  • 25
  • Sorry if I was unclear before, I have edited my question. Could you please have a look again. – Aamu Apr 18 '17 at 17:06
8

This answer was a really good solution to this problem:

You can simply overwrite the get_serializer method in your APIView and pass many=True into get_serializer of the base view like so:

    class SomeAPIView(CreateAPIView):
        queryset = SomeModel.objects.all()
        serializer_class = SomeSerializer

        def get_serializer(self, instance=None, data=None, many=False, partial=False):
            if data is not None:
                data.is_valid(raise_exception=True)
                return super(SomeAPIView, self).get_serializer(instance=instance, data=data, many=True, partial=partial)
            else:
                return super(SomeAPIView, self).get_serializer(instance=instance, many=True, partial=partial)

As mentioned in the original post comments you then also have to call data.is_valid() in cases where a data keyword is passed to the serializer.

Dominik
  • 3
  • 3
Blairg23
  • 11,334
  • 6
  • 72
  • 72
  • How is `data.is_valid()` supposed to work? The data field contains the data passed by the POST, so it's a list of dict, not a serializer. – Hannon Queiroz May 01 '20 at 16:34
  • @HannonCésar I recommend reading up on serializers: https://www.django-rest-framework.org/api-guide/serializers/ `is_valid()` validates that the data being passed is valid serializable data, not that it IS a serializer. A list of dicts is indeed serializable. – Blairg23 May 05 '20 at 17:39
  • I think I didn't phrase my question correctly. I do understand how serializers work. My question is because in your example, the `data` parameter is not a serializer instance, so it has no `.is_valid()` method. The parameter is, in fact, the data being passed in the request. So maybe the approach would be to to assign the result like `s = super(...).get_serializer(...)` and call `s.is_valid(data)` and then return `s`? – Hannon Queiroz May 06 '20 at 07:11
  • @HannonCésar Oh yeah I see what you mean now. I think you're right. In fact, the answer I was copying from has a better answer here: https://stackoverflow.com/a/16697324/1224827 – Blairg23 May 13 '20 at 02:41
5

You can check number of seats in the view function and create one or many tickets:

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def buy_ticket(request):
    # Check if seats is a list
    if isinstance(request.data['seat'], list):
        seats = request.data.pop('seat')
        models = []
        for seat in seats:
            # validate each model with one seat at a time
            request.data['seat'] = seat
            serializer = MovieTicketSerializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            models.append(serializer)
        # Save it only after all seats are valid. 
        # To avoid situations when one seat has wrong id 
        # And you already save previous
        saved_models = [model.save() for model in models]
        result_serializer = MovieTicketSerializer(saved_models, many=True)
        # Return list of tickets
        return Response(result_serializer.data)
    # Save ticket as usual
    serializer = MovieTicketSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data)

It will work but honestly it is such a mess. You can move logic for seats creation in different function so it looks better.

Kirill Vladi
  • 484
  • 6
  • 14
Ivan Semochkin
  • 8,649
  • 3
  • 43
  • 75
1

If you want the user to be able to select multiple seats for one ticket, its probably a good idea to remove the one-one mapping of Seat and MovieTicket, and create a many-one relationship. like so:

Serializers:

class SeatSerializer(serializers.ModelSerializer):
    class Meta:
        model = Seat

class MovieTicketSerializer(serializers.ModelSerializer):
    seats = SeatSerializer(many=True)
    class Meta:
        model = MovieTicket
        fields = '__all__'

    def create(self, vlaidated_data):
        seats = validated_data.pop('seats')
        instance = MovieTicket.objects.create(
            **validated_data)

        for seat in seats:
            Seat.objects.create(
                movieticket=instance, **seats)

        return instance

And the Model should look like:

class MovieTicket(models.Model):
    show = models.ForeignKey(Show)
    user = models.ForeignKey(User)
    purchased_at = models.DateTimeField(default=timezone.now)
    qrcode = models.ImageField(upload_to='qrcode', blank=True, null=True)
    qrcode_data = models.CharField(max_length=255, unique=True, blank=True)

class Seat(models.Model):
    movieticket = ForeignKey(
        MovieTicket, related_name="movieticket")

    # ... other fields.

This would then allow you to pass an array of 'seats' in the request.

smurfMT
  • 124
  • 10
  • Actually, I want to create multiple tickets for each seat. Meaning each seat will have its own Ticket, but the User and the Show will be the same. – Aamu Apr 18 '17 at 10:39
0

If you don't mind adding another app to your django project, you can try with django-rest-framework-bulk, if not you can check the code and see how it was implemented.

If you use this app, you will be able to perform bulk create operations, by sending a list of elements on your POST request.

e.g:

[{'name': 'Jane'}, {'name': 'John'}, {'name': 'Johny'}]
menecio
  • 86
  • 1
  • 5