Search code examples
pythondjangodatabasemany-to-many

django - Many To Many relationship issue


I am working in django, am planning a database for rides for users.

each Ride can have multiple Users (passengers) on it, and multiple middle-destinations in it. also, each Destination can be in multiple Rides and each User (passenger) can be on multiple Rides (overtime).

Also, for each Ride there will be only one final destination and only one driver (also a User).

This is my code:

def get_image_path(models.Model):
    return os.path.join('photos',str(instance.id),filename)


class UserProfile(models.Model):
    user=models.OneToOneField(User)
    phone_number=models.CharField(max_length=12)
    profile_picture=models.ImageField(upload_to=get_image_path, black=True, null=True)

class Ride(models.Model):
    driver=models.ForeignKey(UserProfile, related_name="r_driver")
    destination=models.ForeignKey(Destination, related_name="r_final_destination")
    leaving_time=models.DateTimeField()
    num_of_spots=models.IntergerField()
    passengers=models.ManyToMany(UserProfile, related_name="r_passengers")
    mid_destinations=models.ManyToMany(Destination, related_name="r_mid_destinations")

class Destination(models.Model):
    name=models.CharField(max_length=30)

The Issue is - when a User adds a Ride, I want the driver, destination and mid_destinations and the rest of the fields to be set by the User (the driver is the User adding the Ride), Except for the passengers field. I want the other Users to add themselves to the ride, so when the Ride is created the User (driver) doesn't have to set the passengers.

How do I go about it? and also, any other suggestions about the models?


Solution

  • About the first part of the question

    The Issue is - when a User adds a Ride, I want the driver, destination and mid_destinations and the rest of the fields to be set by the User (the driver is the User adding the Ride), Except for the passengers field.

    The django.forms way

    You just need a custom Form and handle manually the driver field. Something like this:

    class DriverRideForm(forms.ModelForm):
        class Meta:
            model = Ride
            exclude = ('driver', 'passengers', )
    

    And then in the view where request.user is the driver

    form = DriverRideForm(request.POST)
    if form.is_valid():
        ride = form.save(commit=False)
        ride.driver = request.user.userprofile
        ride.save()
    

    The django-rest-framework way

    You can but you will probably do not want to exclude the driver from the serializer fields. This is because usually only one serializer is used per ViewSet so you will want to return that field to the consumer of the API.

    So you have two options:

    • Change the driver field to read-only and hook on perform_create:

      class RideSerializer(serializers.ModelSerializer):
          class Meta:
              model = Ride
              read_only_fields = ('driver', 'passengers')
      
      
      #inside your RideViewSet
      def perform_create(self, serializer): 
           serializer.save(driver=self.request.user.userprofile)
      
    • Add the driver field to the request.data before executing .create without changing the serializer.

      #inside your RideViewSet
      def create(self, request, *args, **kwargs):
          request.data['driver'] = self.request.user.userprofile
          return super(RideViewSet, self).create(request, *args, **kwargs)
      

    How a user can add himself with read-only passengers field?

    I would probably do it with a custom endpoint (with @detail_route). Something like this:

    class RideViewSet(...):
    
        #...
    
        @detail_route(methods=['post'])
        def attach(self, request, pk=None):
            ride = self.get_object()
            ride.passengers.add(request.user.userprofile)
            return Response(self.get_serializer(ride).data)
    

    This way just POST-ing at url like /api/rides/<id>/attach/ will add the logged in request.user to the ride.passengers.

    About the second part of the question

    any other suggestions about the models?

    I think your model structure is fine. I can only think of that in the future you may need more meta data about the relations, so it may be a good moment to be prepared with some Intermediate Models, something like:

    class Ride(models.Model):
        # ...
        passengers=models.ManyToMany(UserProfile, through='RideSeat', related_name="r_passengers")
        mid_destinations=models.ManyToMany(Destination, through='RideMidDestination', related_name="r_mid_destinations")
    
    
    class RideSeat(models.Model):
        ride = models.ForeignKey(Ride)
        passenger = models.ForeignKey(UserProfile)
        seat_number = models.IntegerField()
    
    
    class RideMidDestination(models.Model):
        ride = models.ForeignKey(Ride)
        destination = models.ForeignKey(Destination)
        arrival_time = models.DateTimeField()