I have an AngularJS project that used Django as a framework through the Django Rest Framework (DRF).
I've created a Group model and set up a serializer class for it, however I want to now establish a new field on that model called related_groups
, which would reference the same model (Group) as an array of primary keys.
I don't know if it's possible to self-reference in the serializer, though, and I don't know how else to pass through related groups from the front-end, which can be picked and chosen by the users who own the group. I want that field to reference the primary keys of other Group rows, and iterate through that collection of groups to establish a related group relationship.
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = mod.Group
fields = (
'group_id',
'group_name',
'category',
'related_groups',
)
and the representation appears to be exactly what I want:
GroupSerializer():
group_id = IntegerField(read_only=True)
group_name = CharField(max_length=100)
category = CharField(max_length=256, required=False
related_groups = PrimaryKeyRelatedField(many=True, queryset=Group.objects.all(), required=False)
and the model is represented as such:
class Group(models.Model):
"""
Model definition of a Group. Groups are a collection of users (i.e.
Contacts) that share access to a collection of objects (e.g. Shipments).
"""
group_id = models.AutoField(primary_key=True)
group_name = models.CharField(max_length=100)
owner_id = models.ForeignKey('Owner', related_name='groups')
category = models.CharField(max_length=256)
related_groups = models.ManyToManyField('self', blank=True, null=True)
history = HistoricalRecords()
def __unicode__(self):
return u'%s' % (self.group_name)
def __str__(self):
return '%s' % (self.group_name)
The view accessing this model is pretty simple CRUD view:
@api_view(['GET', 'PUT', 'DELETE'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated, HasGroupAccess))
def group_detail(request, pk, format=None):
group, error = utils.get_by_pk(pk, mod.Group, request.user)
if error is not None:
return error
if request.method == 'GET':
serializer = ser.GroupSerializer(group)
return Response(serializer.data)
elif request.method == 'PUT':
return utils.update_object_by_pk(request, pk, mod.Group,
ser.GroupSerializer)
elif request.method == 'DELETE':
return utils.delete_object_by_pk(request.user, pk, mod.Group)
Which calls some sanitizing and validation methods:
def update_object_by_pk(request, pk, obj_type, serializer):
try:
with transaction.atomic():
obj, error = select_for_update_by_pk(pk, obj_type, request.user)
if error is not None:
return error
obj_serializer = serializer(obj, data=request.data)
if obj_serializer.is_valid():
obj_serializer.save()
else:
response = ("Attempt to serialize {} with id {} failed "
"with errors {}").format(str(obj_type), str(pk),
str(serializer.errors))
return Response(response, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
response = ("Error attempting to update {} with ID={}. user={}, "
"error={}".format(str(obj_type), str(pk),
str(request.user.email), str(e)))
return Response(response, status=status.HTTP_400_BAD_REQUEST)
else:
resp_str = ("Successfully updated {} with ID={}".format(str(obj_type),
str(pk)))
return Response(resp_str, status=status.HTTP_200_OK)
which calls:
def select_for_update_by_pk(pk, mod_type, user):
response = None
obj = None
try:
obj = mod_type.objects.select_for_update().get(pk=pk)
except mod_type.DoesNotExist:
resp_str = ("{} could not be found with ID={}.".
format(str(mod_type), str(pk)))
response = Response(resp_str, status=status.HTTP_404_NOT_FOUND)
return obj, response
which is just a wrapper around select_for_update()
Django method.
The migration created a new table called group_related_groups, with an ID, a from_group and a to_group column, used by Django as a junction / lookup to establish those relationships.
I can write individually to that endpoint, but the serializer GroupSerializer does not seem to want to allow multiple values through by default.
So, using a PUT request to PUT a value of '2' to group with a PK of 1 is successful. However, attempts to put ['2','3']
, [2,3]
, 2,3
and '2','3'
Tracing it back through the view to a utility method, I see that it's serializer.is_valid()
failing the request, so that makes me think it's a many=True
issue, but I don't know which relationship serializer to use for this particular self-referential ManyToManyField problem.
When debugging, I'm outputting the serializer.is_valid() errors to syslog like this:
response = ("Attempt to serialize {} with id {} failed "
"with errors {}").format(str(obj_type), str(pk),
str(serializer.errors))
logger.exception(response)
And I'm getting this exception message as the response:
Message: "Attempt to serialize <class 'bioapi.models.Group'> with id 1 failed with errors
"
The debug error output for obj_serializer.error is
obj_serializer.error of {'related_groups': ['Incorrect type. Expected pk value, received str.']}
And here's debug messasge on request.data:
{'group_name': 'Default Guest Group', 'related_groups': [1], 'group_id': 2, 'category': 'guest'}
which succeeds, and
<QueryDict: {'group_name': ['requestomatic'], 'related_groups':['2,2'], category': ['guest']}>
which fails. Looking at this now, I'm wondering if Postman form-data formatting is the issue. If that's the case I'm going to feel pretty stupid.
Am I able to represent many-to-many relationships from a model back to itself with DRF, or do I need to have a custom serializer just for the relationship table? The documentation for DRF doesn't use self-referential models, and all examples I find online are either using multiple models or multiple Serializers.
Is it possible to use ManyToManyField in my model that is self-referential using the Django Rest Framework (DRF) and its serializers? If so, how?
Looks like a case of nested serialization. The model should be -
class Group(models.Model):
group_id = models.IntegerField(read_only=True)
group_name = models.CharField(max_length=100)
category = models.CharField(max_length=256, required=False)
related_groups = models.ForeignKey('self', required=False, related_name='children')
And the serializers -
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = (
'group_id', 'group_name', 'category', 'related_groups',
)
class NestedGroupSerializer(serializers.ModelSerializer):
children = GroupSerializer(many=True, read_only=True)
class Meta:
model = Group
fields = ('group_id', 'group_name', 'category', 'related_groups', 'children')
You can then use NestedGroupSerializer to access all related groups.
By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved.
Hope that helps.
Try using ModelViewSet as view:
class GroupViewSet(viewsets.ModelViewSet):
queryset = models.Group.objects.all()
serializer_class = serializers.GroupSerializer
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated, HasGroupAccess)
and in your urls.conf
, something like:
from import views
from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'group', views.GroupViewset)
urlpatterns = router.urls
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With