Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django ManyToMany Validation Constraint

I have a ManyToMany link, and a Foreign key which links three objects.

[A]>--<[B]>---[C]

A can belong to many of B, and vice versa. However, A can only belong to B objects with the same parent C.

I'm trying to do something in the clean() method of the model. I'm using Django Rest Framework and no ModelForms or anything like that. I haven't been able to figure it out yet

Simplified Sample Code

class Device(models.Model):
    name = models.CharField(max_length=20)
    projects = models.ManyToManyField(Project, 'devices')
    details = models.CharField(max_length=200)
    serial = models.CharField(max_length=20)
    address models.GenericIPAddressField(default="0.0.0.0")
    port = models.IntegerField(default=3000)
    jumpers = models.IntegerField(default=0)
    install_date = models.DateField(blank=True, null=True)

class Project(models.Model):
    name = models.CharField(max_length=20)
    description = models.CharField(max_length=250)
    area = models.ForeignKey(Area)

class Area(models.Model):
    name = models.CharField(max_length=20)
    description = models.CharField(max_length=250)
    owner = models.CharField(max_length=20)  # microservice doesn't have owner group - field in JWT

Serializers

class AreaSerializer(serializers.ModelSerializer):

    class Meta:
        model = Area
        fields = ('name', 'description', 'owner')


class ProjectSerializer(serializers.ModelSerializer):

    class Meta:
        model = Project
        fields = ('id', 'name', 'description', 'area')


class DeviceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Device
        fields = ('id', 'name', 'projects', 'details', 'serial',
                  'address', 'port', 'jumpers', 'install_date')
like image 750
Edward Williams Avatar asked Feb 01 '18 03:02

Edward Williams


2 Answers

I am not sure where and how do you want to validate your data. So I am just posting the method which can validate if a project can be linked to a device or not based on your specific check.

def validate_project(device, project):
    projects = device.projects.all()
    areas = set(projects.values_list('area', flat=True))
    if len(areas) > 1:
        raise serializers.ValidationError('projects are not valid')        
    return areas.pop() == project.area_id

EDIT:

You have to use a intermediate model for storing the relationship between device and project.

class Membership(models.Model):
    device = models.ForeignKey(Device, on_delete=models.CASCADE)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    area = models.ForeignKey(Area, on_delete=models.CASCADE)

use the above membership model to store the many to many relations.

On your device model use this field to define the many to many relation.

projects = models.ManyToManyField(Project, through='Membership')

checkout the docs

Now when you link a device and project you will have explicitly add the area id as well. Before adding now you can check if the project is valid or not based on the area associated.

like image 181
zaphod100.10 Avatar answered Nov 17 '22 01:11

zaphod100.10


(ignore the wonky field types, cba) enter image description here

What it boils down to is: you need a table BC that stores relations between B and C. Table A would then select only from those relations through the intermediary m2m table ABC (or ditch ABC, couldn't figure out how to draw m2m with the online tool). I think I mixed up B and C in this picture, swap them around depending on whether B or C holds the ForeignKey.
Please correct if I'm wrong!

like image 29
CoffeeBasedLifeform Avatar answered Nov 17 '22 00:11

CoffeeBasedLifeform