0

My question is somewhat related to this one with some differences. I have a model similar to this one:

class Project(models.Model):
    project_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True)
    created_by_id = models.ForeignKey('auth.User', related_name='project', on_delete=models.SET_NULL, blank=True, null=True)
    created_by = models.CharField(max_length=255, default="unknown")
    created = models.DateTimeField(auto_now_add=True)

With the following serializer:

class ProjectSerializer(serializers.ModelSerializer):

    created_by = serializers.ReadOnlyField(source='created_by_id.username')

    class Meta:
        model = Project
        fields = ('project_id', 'created_by', 'created')

And corresponding view:

class projectsView(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

    def perform_create(self, serializer):
        serializer.save(created_by_id=self.request.user)

This code behaves like I want but forces information redundancy and does not leverage the underlying relationnal database. I tried to use the info from the linked question to achieve a "write user id on database but return username on "get"" in a flat json without success:

Removing the "created_by" field in the model. Replacing the serializer with:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User


class ProjectSerializer(serializers.ModelSerializer):

    created_by = UserSerializer(read_only=True)
    created_by_id = serializers.PrimaryKeyRelatedField(
    queryset=User.objects.all(), source='created_by', write_only=True)

    class Meta:
        model = Project
        fields = ('project_id', 'created_by', 'created_by_id', 'created')

Which would NOT 100% give me what I want, i.e. replace the user id with the username in a flat json but return something like: {'project_id': <uuid>, 'created_by': <user json object>, 'created': <data>}. But still I get a {'created_by_id': ['This field is required.']} 400 error.

Question: How can I write a user id to a database object from the request.user information to refer to an actual user id but return a simple username in the GET request on the projectsView endpoint without explicitly storing the username in the Model? Or more generally speaking, how can I serialize database objects (Django models) into customer json response by using default serialization DRF features and default DRF views mixins?

Alternate formulation of the question: How can I store an ID reference to another DB record in my model (that can be accessed without it being supplied by the payload) but deserialize a derived information from that object reference at the serializer level such as one specific field of the referenced object?

Sebastien
  • 1,439
  • 14
  • 27
  • If you want to store the current user, remove it from serializer and then populate your model from view. if this is your answer, tell me to complete it – Mohammad Efazati Jul 09 '18 at 14:50
  • Look at the view snippet I posted. Sounds like you are not answering the question at all. – Sebastien Jul 09 '18 at 16:56
  • What I mean is why you use the same serializer for the post and get, you can write another serializer for get and in that one bring all detail for user, and for the post also you can remove user_id and then store in DB with another method – Mohammad Efazati Jul 09 '18 at 17:19
  • My understanding of DRF serializers is that there is a way to define serialization and deserialization from it and abstract away the models from the view which feels like the exact opposite of what you seem to be suggesting. But I may be misunderstanding something... – Sebastien Jul 09 '18 at 19:36

2 Answers2

0

I would recommend you to use Two different serializers for Get and POST operations. Change your serializers.py as

class ProjectGetSerializer(serializers.ModelSerializer):
    created_by_id = serializers.StringRelatedField()

    class Meta:
        model = Project
        fields = '__all__'


class ProjectCreateSerializer(serializers.ModelSerializer):
    created_by_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault())

    def create(self, validated_data):
        return Project.objects.create(**validated_data, created_by=validated_data['created_by_id'].username)

    class Meta:
        model = Project
        fields = '__all__'


Also, I reccomend ModelViewSet for API class if you are looking for CRUD operations. Hence the view will be like this,

class projectsView(viewsets.ModelViewSet):
    queryset = Project.objects.all()

    def get_serializer_class(self):
        if self.action == 'create':
            return ProjectCreateSerializer
        return ProjectGetSerializer


So, the payload to create Project is,

{
}

One thing you should remember, while you trying to create Project user must logged-in

UPDATE - 1
serializer.py

class ProjectCreateSerializer(serializers.ModelSerializer):
    created_by_id = serializers.StringRelatedField()

    class Meta:
        model = Project
        fields = '__all__'

    def create(self, validated_data):
        return Project.objects.create(**validated_data, created_by_id=self.context['request'].user)

views.py

class projectsView(viewsets.ModelViewSet):
    queryset = Project.objects.all()
    serializer_class = ProjectCreateSerializer
JPG
  • 82,442
  • 19
  • 127
  • 206
  • The payload to create you wrote is wrong. Your view is missing critical aspects of mine. The user reference comes from who's "logged in" not from the payload at all. – Sebastien Jul 10 '18 at 11:36
  • That is, If you POST empty data to the API, It should identify the `created_by` also .. right? – JPG Jul 10 '18 at 11:38
  • An empty POST creates a user ref, a project id and a project creation datetime. In other words, the "create" payload is empty. – Sebastien Jul 10 '18 at 11:39
  • You could do it by overriding `create()` method of `ProjectCreateSerializer` serializer. – JPG Jul 10 '18 at 11:45
  • hence the payload wil be an empty dict/JSON object as `{}` – JPG Jul 10 '18 at 11:46
  • My main goal is not to "make it work". The goal is to figure out how to do it at the serialization level and/or why it is not possible. This IS a textbook example of serialization: get a json, convert to DB format and store; read from DB and formulate json for replies. If/When I want to use nested jsons, I will have to customize every view. Not good... – Sebastien Jul 10 '18 at 11:49
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/174718/discussion-between-sebastien-and-jerin-peter-george). – Sebastien Jul 10 '18 at 11:54
  • What you mean by `it is not possible` ? If you wish to use Nested Json, you could do that on serializer level. – JPG Jul 10 '18 at 11:54
  • I found the problem and posted my answer – Sebastien Jul 10 '18 at 14:19
0

The error is in the write_only field options. The required parameter default value is set to True while the intent is to not make it required if we take a look at the model. Here in the view, I use the perform_create as post processing to save on the Model DB representation. Since required default value is True at the creation level, the first .save() to the DB fails. Since this is purely internal logic, the required is not necessary. So simply adding the required=False option on the PrimaryKeyRelatedField does the job:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User


class ProjectSerializer(serializers.ModelSerializer):

    created_by = UserSerializer(read_only=True)
    created_by_id = serializers.PrimaryKeyRelatedField(
    queryset=User.objects.all(), source='created_by', write_only=True, required=False)

    class Meta:
        model = Project
        fields = ('project_id', 'created_by', 'created_by_id', 'created')

Enforcing the required=True at the Model level as well would require to override the .save function of the serializer if I insist on playing with the logic purely at the serializer level for deserialization. There might be a way to get the user ref within the serializer as well to keep the views implementation even more 'default'... This can be done by using the default value from Jerin:

class ProjectSerializer(serializers.ModelSerializer):

    created_by = UserSerializer(read_only=True)
    created_by_id = serializers.PrimaryKeyRelatedField(
    queryset=User.objects.all(), source='created_by', 
                           write_only=True,
                           required=False,
                           default=serializers.CurrentUserDefault())

    class Meta:
        model = Project
        fields = ('project_id', 'created_by', 'created_by_id', 'created')

Now to flaten the json with username only, you need to use a slug field instead of the UserSerializer:

class ProjectSerializer(serializers.ModelSerializer):

    created_by = serializers.SlugRelatedField(
      queryset=User.objects.all(), slug_field="username")
    created_by_id = serializers.PrimaryKeyRelatedField(
      queryset=User.objects.all(), source='created_by', write_only=True, required=False)

    class Meta:
        model = Project
        fields = ('project_id', 'created_by', 'created_by_id', 'created')

And then only the username field value of the User Model will show at the create_by json tag on the get payload.

UPDATE - 1

After some more tweaking here is the final version I came up with:

class ProjectSerializer(serializers.ModelSerializer):

    created_by_id = serializers.PrimaryKeyRelatedField(
        queryset=User.objects.all(), write_only=True, required=False, default=serializers.CurrentUserDefault())
    created_by = serializers.SerializerMethodField('creator')

    def creator(self, obj):
        return obj.created_by_id.username

    class Meta:
        model = Project
        fields = ('project_id', 'created_by_id', 'created_by', 'created')
Sebastien
  • 1,439
  • 14
  • 27