1

So the back story is I have a model that contains 3 fields. One is just filled in with a default, the other is passed in with the API request, the last is calculated in the view. I can't figure out how to get both the request.data and the calculated value into the serializer. Here is my model:

import re
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db import models
from django.utils import timezone

URL_MAX_SHORT_ID_LEN = 12
# Maximum length for IE and several other search engines and protocols is
# 2047/2048.
URL_MAX_URL_LEN = 2047

class URL(models.Model):
    short_id = models.CharField(max_length=URL_MAX_SHORT_ID_LEN, primary_key=True)
    url = models.URLField(max_length=URL_MAX_URL_LEN)
    added_date = models.DateTimeField('date added',
                                      default=timezone.now)

    def clean(self):
        # TODO: make max length and possible chars a setting or global variable
        if re.match('^[a-zA-Z0-9]{1,' + str(URL_MAX_SHORT_ID_LEN) + '}$', self.short_id) is None:
            raise ValidationError({'short_id':
                'only a-zA-Z0-9 valid chars and max length of ' +
                                   str(URL_MAX_SHORT_ID_LEN)
            })

    def save(self, *args, **kwargs):
        self.full_clean()
        super(URL, self).save(*args, **kwargs)

    def __str__(self):
        return str(self.__class__) + ': ' + str(self.__dict__)

Here's is my serializer:

from rest_framework import serializers
from url_shortener.models import URL

class URLSerializer(serializers.ModelSerializer):
    short_id = serializers.SerializerMethodField()

    class Meta:
        model = URL
        fields = ('short_id', 'url', 'added_date')
        read_only_fields = ('added_date',)

    def get_short_id(self, obj):
        return self.context.get('short_id')

Testing in the shell throws an error saying that short_id is empty:

>>> from url_shortener.serializers import *
>>> from django.utils.six import BytesIO
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser
>>>
>>> j = b'{"url":"https://gobin.io"}'
>>> stream = BytesIO(j)
>>> data = JSONParser().parse(stream)
>>> s = URLSerializer(data=data, context={'short_id': 'asd123'})
>>> s.is_valid()
True
>>> s.validated_data
OrderedDict([('url', 'https://gobin.io')])
>>> s.save()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/jolly/git/lurl/myenv/lib/python3.5/site-packages/rest_framework/serializers.py", line 215, in save
    self.instance = self.create(validated_data)
  File "/Users/jolly/git/lurl/myenv/lib/python3.5/site-packages/rest_framework/serializers.py", line 916, in create
    instance = ModelClass.objects.create(**validated_data)
  File "/Users/jolly/git/lurl/myenv/lib/python3.5/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/jolly/git/lurl/myenv/lib/python3.5/site-packages/django/db/models/query.py", line 394, in create
    obj.save(force_insert=True, using=self.db)
  File "/Users/jolly/git/lurl/lurl/url_shortener/models.py", line 27, in save
    self.full_clean()
  File "/Users/jolly/git/lurl/myenv/lib/python3.5/site-packages/django/db/models/base.py", line 1248, in full_clean
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'short_id': ['This field cannot be blank.', 'only a-zA-Z0-9 valid chars and max length of 12']}

It seems that get_short_id never gets called. I put a raise in it, and it never got thrown. I'm not sure what I'm doing wrong or if this is the right way to do what I'm doing.

Luke Jolly
  • 11
  • 2

2 Answers2

1

SerializerMethodField is read only. You can use normal CharField and pass it's value to serializer save method in view instead:

class URLSerializer(serializers.ModelSerializer):
    short_id = serializers.CharField()

in view:

s = URLSerializer(data=data)
s.is_valid()
s.save(short_id='asd123')
neverwalkaloner
  • 46,181
  • 7
  • 92
  • 100
  • I started off doing pretty much that. When I run s.is_valid() it is false and thus s.save() errors. This is my serializer using your suggestions https://gobin.io/ECcE – Luke Jolly Jun 29 '17 at 02:41
  • @LukeJolly try to make short_id optional: `serializers.CharField(required=False)` – neverwalkaloner Jun 29 '17 at 02:45
0

I finally found the right search words and found a similar stackoverflow. I'm not sure if it's always correct, but in my case short_id is actually read_only. So I made my my serializer with extra_kwargs:

from rest_framework import serializers
from url_shortener.models import URL, URL_MAX_SHORT_ID_LEN

class URLSerializer(serializers.ModelSerializer):
    class Meta:
        model = URL
        fields = ('short_id', 'url', 'added_date')
        read_only_fields = ('added_date',)
        extra_kwargs = {
            'short_id' : {'read_only' : True}
        }

and then validated and saved it like this:

s = URLSerializer(data=data)
s.is_valid()
s.save(short_id='asd123')
Luke Jolly
  • 11
  • 2