Search code examples
pythondjangodjango-serializer

Django - Serializer throwing "Invalid pk - object does not exist" when setting ManyToMany attribute where foreign keyed object does exist


So below I have some code that tests the functionality where someone creates a post and that post has a hash_tag which is "#video" in this case. The code takes the Post body and uses regex to find any word that starts with "#". If it does then it creates or gets that HashTag from the HashTag table. Then sets that list of HashTag to the hash_tags attribute under Post.

For some reason the CreatePostSerializer serializer is throwing an exception that doesn't make sense. The serializer is throwing the exception ValidationError({'hash_tags': [ErrorDetail(string='Invalid pk "[\'video\']" - object does not exist.', code='does_not_exist')]}). The reason this doesn't make sense is because when I debug and set a breakpoint right after except Exception as e under views.py this is what I get

>>>e
ValidationError({'hash_tags': [ErrorDetail(string='Invalid pk "[\'video\']" - object does not exist.', code='does_not_exist')]})
>>>HashTag.objects.get(pk='video')
<HashTag: HashTag object (video)>
>>>request.data['hash_tags']
['video']

So the >>> represents what I input into the debugger. I'm essentially stopped at the line return Response... and we can see e is the ValidationError I mentioned, but we can see that the object it claims doesn't exist does indeed exist. Why is the serializer throwing a "ValidationError - object does not exist" when it does?

Note: I have another test that does exactly the same thing and passes except no video file is being passed this leads me to believe that Django is doing something different in the case that the incoming body is multi-part. I also tried in the instance that there is only one hash tag to set hash_tags=<single hash tag> rather than a list and it worked. This is a hack though and cleaner solution is preferred.

helpers.py

import re

def extract_hashtags(text):
    regex = "#(\w+)"
    return re.findall(regex, text)

test.py

def test_real_image_upload_w_hash_tag(self):
    image_file = retrieve_test_image_upload_file()
    hash_tag = 'video'
    response = self.client.post(reverse('post'),
                                data={'body': f'Some text and an image #{hash_tag}',
                                      'images': [image_file]},
                                **{'HTTP_AUTHORIZATION': f'bearer {self.access_token}'})
    self.assertEqual(response.status_code, status.HTTP_201_CREATED)

views.py

def set_request_data_for_post(request, user_uuid: str):
    request.data['creator'] = user_uuid
    post_text = request.data['body']
    hash_tags_list = extract_hashtags(post_text)
    hash_tags = [HashTag.objects.get_or_create(hash_tag=ht)[0].hash_tag for ht in hash_tags_list]

    if len(hash_tags) > 0:
        request.data['hash_tags'] = hash_tags

    return request

def create_post(request):
    user_uuid = str(request.user.uuid)
    request = set_request_data_for_post(request=request, user_uuid=user_uuid)

    try:
        serializer = CreatePostSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            post_obj = serializer.save()
    except Exception as e:
        return Response(dict(error=str(e),
                             user_message=error_message_generic),
                        status=status.HTTP_400_BAD_REQUEST)

    return Response(serializer.data, status=status.HTTP_201_CREATED)

serializer.py

from rest_framework import serializers
from cheers.models import Post

class CreatePostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('creator', 'body', 'uuid', 'created', 'updated_at', 'hash_tags')

model.py

class Post(models.Model):
    # ulid does ordered uuid creation
    uuid = models.UUIDField(primary_key=True, default=generate_ulid_as_uuid, editable=False)
    created = models.DateTimeField('Created at', auto_now_add=True)
    updated_at = models.DateTimeField('Last updated at', auto_now=True, blank=True, null=True)
    creator = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="post_creator")
    body = models.CharField(max_length=POST_MAX_LEN, validators=[MinLengthValidator(POST_MIN_LEN)])
    hash_tags = models.ManyToManyField(HashTag, blank=True)

class HashTag(models.Model):
    hash_tag = models.CharField(max_length=HASH_TAG_MAX_LEN, primary_key=True, validators=[
        MinLengthValidator(HASH_TAG_MIN_LEN)])

Solution

  • under your test/__init__.py you have to add these lines

    from django.db.backends.postgresql.features import DatabaseFeatures
    
    DatabaseFeatures.can_defer_constraint_checks = False
    

    There's some weird internal bug where if you operate on one table a lot with a lot of different TestCase classes then it'll do a DB check at the end after it's been torn down and it'll cause an error.

    I'm also using factory boy (https://factoryboy.readthedocs.io/en/stable/orms.html) to generate my test DB, which is the main reason this issue arises. The reason I believe this is because I switched out factory boy for just using <model>.objects.create() and my tests stopped failing.