DRF- Ошибка при создании нового экземпляра в модели M2M через - PullRequest
2 голосов
/ 26 мая 2020

У меня есть две следующие модели:

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

и

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

, которые смоделированы отношением «многие ко многим», используя эту сквозную модель:

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"

Вот мое мнение:

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)

Проблема, с которой я сталкиваюсь, заключается в том, что я хочу отправить данные POST следующего формата в таблицу M2M:

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

Вышеупомянутое содержит список многих статей, поэтому я использовал custom field в моем serializer, чтобы извлечь каждый article, создать новый объект UserArticle и вставить его, используя bulk_create, в мою таблицу M2M . Я думаю, что это способ go, когда входящие данные не точно соответствуют модели БД, но я могу ошибаться. Так что прокомментируйте, если вы видите что-то не так с этим подходом.

Вот сериализатор:

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))

и

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

Кажется, это работает нормально, поскольку я вижу, что данные правильно вставлены в таблицу UserArticle:

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

Проблема возникает, когда код достигает этой строки:

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

и, более конкретно, я получаю ошибку:

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'.

Оригинал ошибка исключения возникает в строке instance = getattr(instance, attr) функции def get_attribute(instance, attrs) в источнике fields.py DRF.

Что мне здесь не хватает?

1 Ответ

2 голосов
/ 26 мая 2020

Прежде всего, нет причин вызывать метод save для каждого из массово созданных экземпляров.

Второй - причина исключения. Вы вызываете метод create viewset. он вызывает метод сериализаторов create, который должен возвращать только один экземпляр (созданный объект). но ваш сериализатор возвращает список created_user_articles. В списке действительно нет поля posted_as.

Итак, есть два способа исправить.

  1. Первый - переопределить метод create в представлении, чтобы изменить способ представления данных. Например, используйте другой сериализатор для данных ответа:

    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. Второй - возвращает только один экземпляр в create методе вашего сериализатора.

...