kingJulian
kingJulian

Reputation: 6180

DRF- Error when creating a new instance in an M2M through model

I have the following two models:

class User(models.Model):
    user_id = models.CharField(
        max_length=129,
        unique=True,
    )
    user_article = models.ManyToManyField(
        Article,
        through="UserArticle",
    )
    occupation = models.CharField(max_length=100, default='null')

    def __str__(self):
        return self.user_id

and

class Article(models.Model):
    uuid = models.UUIDField(editable=False, unique=True)
    company = models.ForeignKey(
        Company,
        on_delete=models.PROTECT,
        related_name='article_company_id',
    )
    articleType = models.ForeignKey(
        ArticleType,
        on_delete=models.PROTECT,
        related_name='type',
    )    
    date_inserted = models.DateField()    
    def __str__(self):
        return self.uuid

which are modeled with a many-to-many relationship, using this through model:

class UserArticle(models.Model):    
    user = models.ForeignKey(User, to_field='user_id',
                             on_delete=models.PROTECT,)
    article = models.ForeignKey(Article, to_field='uuid',
                                 on_delete=models.PROTECT,)
    posted_as = ArrayField(
        models.CharField(max_length=100, blank=True),)
    post_date = models.DateField()

    class Meta:
        db_table = "core_user_articles"

Here's my view:

class BatchUserArticleList(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        generics.GenericAPIView):
    queryset = UserArticle.objects.all()
    serializer_class = BatchUserArticleSerializer

    def create(self, request, *args, **kwargs):
        serializer = BatchUserArticleSerializer(data=request.data)
        if not serializer.is_valid():
            return response.Response({'Message': 'POST failed',
                                  'Errors': serializer.errors},
                                 status.HTTP_400_BAD_REQUEST)
        self.perform_create(serializer)  # equal to serializer.save()
        return response.Response(serializer.data, status.HTTP_201_CREATED)

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

The problem I'm facing is when I want to POST data, of the following format, in the M2M table:

{
    "posted_as": ["news"],
    "post_date": "2020-05-26",
    "user": "jhtpo9jkj4WVQc0000GXk0zkkhv7u",
    "article": [
        "11111111",
        "22222222"
    ]
}

The above contains a list of many articles so I used a custom field in my serializer in order to extract each article, create a new UserArticle object and insert it, using bulk_create, into my M2M table. I think that's the way to go when the incoming data do not map exactly to the DB model, but I might be wrong. So please comment if you see something off with this approach.

Here is the serializer:

class BatchUserArticleSerializer(serializers.ModelSerializer):
    article= ArticleField(source='*') #custom field

    class Meta:
        model = UserArticle
        fields = ('posted_as', 'post_date', 'user', 'article')

    def validate(self, data):    
        post_date = data['post_date']
        if post_date != date.today():
            raise serializers.ValidationError(
                'post_date: post_date is not valid',
            )
        return data

    def create(self, validated_data):
        post_as = list(map(lambda item: item, validated_data['posted_as']))
        post_date = validated_data['post_date']
        user = validated_data['user']
        list_of_articles = validated_data['article']            
        user_object = User.objects.get(user_id=user)
        articles_objects = list(map(lambda res: Article.objects.get(uuid=res), list_of_articles))    
        user_articles_to_insert = list(map(
            lambda article: UserArticle(
                posted_as=posted_as,
                post_date=post_date,
                article=article,
                user=user_object),
            articles_objects))

        try:
            created_user_articles = UserArticles.objects.bulk_create(user_articles_to_insert)
            for res in created_user_articles:
                res.save()
            return created_user_articles
        except Exception as error:
            raise Exception('Something went wrong: {0}'.format(error))

and

class ArticleField(serializers.Field):
    def to_representation(self, value):
        resource_repr = [value.article]
        return resource_repr

    def to_internal_value(self, data):
        internal_repr = {
            'article': data
        }
        return internal_repr

This seems to work ok as I can see data being correctly inserted in the UserArticle table:

id | posted_as | post_date | user | article
1  | news      | 2020-05-26 | jhtpo9jkj4WVQc0000GXk0zkkhv7u | 11111111
2  | news      | 2020-05-26 | jhtpo9jkj4WVQc0000GXk0zkkhv7u | 22222222

The problem comes when code reaches this line:

response.Response(serializer.data, status.HTTP_201_CREATED)

and more specifically, the error I'm getting is:

AttributeError: Got AttributeError when attempting to get a value for field `posted_as` on serializer `BatchUserArticleSerializer`.

The serializer field might be named incorrectly and not match any attribute or key on the `list` instance. Original exception text was: 'list' object has no attribute 'posted_as'.

The original exception error is raised at the instance = getattr(instance, attr) line of the def get_attribute(instance, attrs) function in the fields.py DRF source.

What am I missing here?

Upvotes: 2

Views: 242

Answers (1)

ncopiy
ncopiy

Reputation: 1604

First of all, there is no reason to call save method for each of bulk-created instances.

Second one is reason of exception. You call create viewset method. it calling serializers create method which must return only one instance (created object). but your serializer returns list created_user_articles. List really have no field posted_as.

So, there is two ways to fix it.

  1. First one is override create method in view, to change the way of data representation. For ex. use another serializer for response data:

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        created_user_articles = self.perform_create(serializer)
    
        # use another way to get representation
        response_data = AnotherUserArticleSerializer(created_user_articles, many=True).data
        return Response(response_data, status=status.HTTP_201_CREATED, headers=headers)
    
    def perform_create(self, serializer):
        # add return to get created objects
        return serializer.save()
    
  2. Second one is return only one instance in create method of your serializer.

Upvotes: 2

Related Questions