1

I start to create REST API for my web-application with Django and Django rest framework and I need one logic problem.

There are entities Instruction and Tag. The user visit my service and create self Instruction and add exists Tag OR new Tag for it.

I created my model seriallizer class with using PrimaryKeyRelatedField for relation Instruction<->Tag. But if I do POST for a new Instruction with new Tag I got error: "Invalid pk \"tagname\" - object does not exist.". I solved this problem with the overriding of the to_internal_value method in my field class.

What is the best practice for solving this problem? It seems to me this problem is typical for web and REST API.

My models:

class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

my serializers:

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('name',)

class InstructionSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    author = serializers.SerializerMethodField()

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
        read_only_fields = ('modified_datetime',)

I created new field class class PrimaryKeyCreateRelatedField and overrided to_internal_value method for creating the new Tag object instead raising with message 'does_not_exist':

PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)

my view:

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk

        serializer = InstructionSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Update

models.py

alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
                              _('Only alphanumeric characters are allowed.'))


class Tag(Model):
    name = CharField(max_length=32, verbose_name=_("Name"),
                     unique=True, validators=[alphanumeric], primary_key=True)

    def __str__(self):
        return self.name


class Step(PolymorphicModel):
    instruction = ForeignKey(Instruction,
                             verbose_name=_("Instruction"),
                             related_name='steps',
                             blank=False, null=False,
                             on_delete=CASCADE)
    position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)

    description = TextField(verbose_name=_("Description"),
                            max_length=2048,
                            blank=False, null=False)

    class Meta:
        verbose_name = _("Step")
        verbose_name_plural = _("Steps")
        ordering = ('position',)
        unique_together = ("instruction", "position")

    def __str__(self):
        return self.description[:100]


class Instruction(Model):
    user = ForeignKey(settings.AUTH_USER_MODEL,
                      related_name='instructions',
                      on_delete=CASCADE,
                      blank=False, null=False,
                      verbose_name=_("User"))
    title = CharField(max_length=256,
                      verbose_name=_("Title"),
                      blank=False, null=False)
    created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
    modified_datetime = DateTimeField(
        verbose_name=_("Last modification time"), blank=False, null=False)
    tags = ManyToManyField(Tag,
                           related_name="instructions",
                           verbose_name=_("Tags"))

    # thumbnail = #TODO: image field

    class Meta:
        ordering = ['-created_datetime']
        # singular_name = _("")

    def save(self, force_insert=False, force_update=False, using=None,
             update_fields=None):
        n = now()
        if self.id is None:
            self.created_datetime = n
        self.modified_datetime = n
        super(Instruction, self).save(force_insert, force_update, using, update_fields)

    def __str__(self):
        return self.title

views.py

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def get_serializer_class(self):
        """Return different serializer class for different action."""
        if self.action == 'list':
            return InstructionSerializer
        elif self.action == 'create':
            return InstructionCreateSerializer

serialiers.py

class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):

    def to_internal_value(self, data):
        if self.pk_field is not None:
            data = self.pk_field.to_internal_value(data)
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            # self.fail('does_not_exist', pk_value=data)
            return self.get_queryset().create(pk=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)


class InstructionCreateSerializer(serializers.ModelSerializer):
    tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
        read_only_fields = ('modified_datetime',)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')

        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
    # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)

        for tag in tags_data:
            instruction.tags.add(tag)

        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction


class InstructionSerializer(serializers.ModelSerializer):
    tags = serializers.StringRelatedField(many=True)
    author = serializers.SerializerMethodField()
    steps = InstructionStepSerializer(many=True)

    def get_author(self, obj):
        return obj.user.username

    class Meta:
        model = Instruction
        fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
        read_only_fields = ('modified_datetime',)
Stan Zeez
  • 1,138
  • 2
  • 16
  • 38

4 Answers4

5

In my case to solve the problem I need to override the method run_validation. That allow make check of tags and create their (if not exists) before validation.

class InstructionCreateSerializer(serializers.ModelSerializer):
    steps = InstructionStepSerializer(many=True)
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Instruction
        fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
        read_only_fields = ('modified_datetime',)

    def run_validation(self, data=serializers.empty):
        if 'tags' in data:
            for tag in data['tags']:
                Tag.objects.get_or_create(name=tag)
        return super(InstructionCreateSerializer, self).run_validation(data)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        steps_data = validated_data.pop('steps')

        # NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
        # "needs to have a value for field "id" before this many-to-many relationship can be used."
        instruction = Instruction.objects.create(**validated_data)

        for tag in tags_data:
            instruction.tags.add(tag)

        for step in steps_data:
            Step.objects.create(instruction=instruction,
                                description=step['description'],
                                position=step['position'])
        return instruction
Stan Zeez
  • 1,138
  • 2
  • 16
  • 38
2

Apart from the answers given by @YPCrumble and @SijanBhandari, I just had to comment on something in your code.

In the models.py, you have overridden the save method for adding created_at and modified_on. For that you could just add

created_at = models.DateTimeField(auto_now_add=True)
modified_on = DateTimeField (auto_now=True)

The auto_now_add option sets when the object is created for the first time. It's not editable. The auto_now setting sets whenever the object is saved, ie, whenever object.save() method is called upon.

These usually are used for timestamping the objects for future references.

Why write so many lines, when you could do this on just 2 lines of code. Just a heads up though!!

For further details, go to the documentation here

zaidfazil
  • 9,017
  • 2
  • 24
  • 47
1

In "regular" Django you usually want to create your model instance in the form's save method, not the view. DRF is similar, in that you want to create your model instances in the serializer's create or update methods. The reason for this is that if you need to add a new endpoint to your API you can reuse the serializer and would not have to write duplicate code creating or updating your model instance.

Here's how I'd refactor your code:

  • Remove the entire create method from your ModelViewSet - you don't need to override that.
  • Remove the custom PrimaryKeyCreateRelatedField - you just need a PrimaryKeyRelatedField
  • Add two methods to your serializer - create and update:
    • In the create method, create your tag objects before saving the instruction object like you can see in the DRF docs. You can get the current user like you were doing in your view via self.context['request'].user in this create method. So you might create the Instruction like Instruction.objects.create(user=self.context['request'].user, **validated_data) and then loop through the tags (like they do for tracks in the docs) to add them to the Instruction.
    • The docs don't have an example update method but essentially your update method also takes an instance parameter for the existing instruction. See this answer from the creator of DRF for more details
Community
  • 1
  • 1
YPCrumble
  • 26,610
  • 23
  • 107
  • 172
  • Thank you for your answer. I was try your solution (moved the creation of tags from my ModelViewSet into my InstructionSerializer) but I get error: "user": ["This field is required."]. I thik it is because the validation does before calling of the create/update method in serializer. When does a validation the field 'user' is empty yet. How to solve it? – Stan Zeez May 14 '17 at 21:04
  • @StanZeez if you are adding the `user` via the logged-in user you should remove "user" from your `InstructionSerializer.Meta.fields` declaration. – YPCrumble May 14 '17 at 21:12
  • I understood you, but if I will remove "user" from my Instruction Serializer how I can show of the author of instructions in **GET** response? Maybe use different serializers for **create** and **list** actions by means of overriding for **get_serializer_class** method in InstructionViewSet or not? – Stan Zeez May 14 '17 at 21:26
  • 1
    Right now you're using the `get_author` method for that. One option is to use [CurrentUserDefault](http://www.django-rest-framework.org/api-guide/validators/#currentuserdefault) to use the current user as the author of the field. – YPCrumble May 14 '17 at 22:00
  • please see my Update (I add Step model for complete the picture). It is that I got after all changes. I still have the problem of PrimaryKeyRelease Relied. If I change my custom PrimaryCareReleaseField to serializers.PrimaryKeRelatedField, I get an error: `"tags": [ "Invalid pk \"tagname\" - object does not exist."]`. One solution is to use TagSerializer field instead PrimaryKeRelatedField-based field. But then I have to pass in POST tag object as {"name": "tagname"} instead simply string "tagname". – Stan Zeez May 15 '17 at 22:01
  • @StanZeez it looks like you're trying to POST "tagname" rather than the tag's pk (usually an integer). If this is the case, you could try using [`SlugRelatedField`](http://www.django-rest-framework.org/api-guide/relations/#slugrelatedfield) and should be able to simply post the tagname as you're hoping to. – YPCrumble May 16 '17 at 01:34
  • thanks you for your advice with SlugRelatedField. I was try it as `tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=Tag.objects.all())`. But it doesn't help for me. I was get error: `"tags": ["Object with name=dev609 does not exist."]`. It is similar for error with using `PrimaryKeRelatedField`. But I found solution that halp for me (see my answer). – Stan Zeez May 16 '17 at 21:17
0

The best way would be sort out everything at your CREATE method of the view.

I believe you tags will be sent from your front-end to the back-end at the format of

[   1,
    {'name': "TEST"},
    {'name': 'TEST2'}
]

Here '1' is the existing tag id and 'TEST' and 'TEST2' are the two new tags inserted by the user. Now you can change your CREATE method as follows:

class InstructionViewSet(viewsets.ModelViewSet):
    queryset = Instruction.objects.all()
    serializer_class = InstructionSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    def create(self, request, *args, **kwargs):
        data = dict.copy(request.data)
        data['user'] = self.request.user.pk

        # MODIFICATION.....
        tags = self.request.get('tags', None)
        tag_list = []
        if tags:
            for tag in tags:
                if isinstance(tag, dict):
                    new_tag = Tag.objects.create(name=tag['name'])
                    tag_list.append(new_tag.id)
                else:
                    tag_list.append(int(tag))


        data = {

        'title': ....
        'tags': tag_list,
        'user': ...
        'author': ...
        ......



        }

        serializer = InstructionSerializer(data=data)

I hope it will be helpful for you.

Sijan Bhandari
  • 2,941
  • 3
  • 23
  • 36