7

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?

Jon Mitten
  • 1,965
  • 4
  • 25
  • 53
  • please explain what you mean by `GroupSerializer():` in your second snippet – e4c5 Oct 07 '16 at 04:26
  • 1
    Once `serializer.is_valid()` failed, what is the content of `serializer.errors` ? It usually gives good hints why it is failing. – Michael Rigoni Oct 07 '16 at 08:14
  • @e4c5, inspecting the serializer relation with the method described in the serializer relations documentation, here: http://www.django-rest-framework.org/api-guide/relations/#inspecting-relationships , and the relationship appears to be in order. – Jon Mitten Oct 07 '16 at 14:30
  • @Michael, I've updated my question to include debugging error response – Jon Mitten Oct 07 '16 at 16:26
  • @Smittles `serializer` seems to be the class, not the serializer instance... What is the code of the view you are using ? – Michael Rigoni Oct 10 '16 at 12:03
  • @Michael, updated to include the view and support methods. Still not identifying the issue, even after 3 days. – Jon Mitten Oct 11 '16 at 04:45
  • @Smittles, what is the content of `str(obj_serializer.errors)` (not `str(serializer.errors)` which gets called on the class). Also, is there a reason you are coding all the REST action instead of using the framework (ModelViewSet for example)? – Michael Rigoni Oct 11 '16 at 06:56
  • I've updated the question to include that error message, and here it is again: `obj_serializer.error of {'related_groups': ['Incorrect type. Expected pk value, received str.']}` – Jon Mitten Oct 12 '16 at 17:57
  • @Smittles and what is the value of `request.data` then (especially `request.data['related_groups']`)? There is something the serializer does not like... – Michael Rigoni Oct 13 '16 at 07:13
  • @Michael, I've added the `request.data` value, which shows me that the dictionary I intend to be sending to Django is being interpreted as a string - and I think the issue may be Postman (the REST client I've started using for REST framework development) may be the culprit. I will attempt to send a different format through cURL... – Jon Mitten Oct 13 '16 at 20:34

2 Answers2

3

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.

user6399774
  • 116
  • 6
  • This methodology fails on validity, as does mine. serializer.is_valid() comes back as false when passing `[2,3]` or `2,3`. However, the experience is the same when passing a single integer like `2` or `3` by itself – Jon Mitten Oct 06 '16 at 17:49
  • I already have a field here for related_groups - and a table created for the ManyToManyField - so why do I need 'children'? why do I need a related_name if I've got a ManyToManyField related back to the same model? – Jon Mitten Oct 12 '16 at 06:27
  • Also, I need to have more than one related group, hence the ManyToManyField usage. Using ManyToManyField creates a relationship table in the database - perfect for quick relationships matching where one NightClub group may have several DJ groups, where NightClub has one set of members and DJ 1 has another set, and DJ 2 has yet another set. – Jon Mitten Oct 12 '16 at 06:58
1

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
Michael Rigoni
  • 1,946
  • 12
  • 17