Extra Fields on POST

3,770 views
Skip to first unread message

Carlton Gibson

unread,
Nov 16, 2012, 11:08:56 AM11/16/12
to django-res...@googlegroups.com
Hi All,

Following the tutorial we have a serializer and view:

class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')

class UserList(generics.ListCreateAPIView):
"""
API endpoint that represents a list of users.
"""
model = User
serializer_class = UserSerializer

We'd like to allow the creation of a User with a password field but (obviously) not reveal this over the API.

1. (How) can we allow the extra filed on POST (or do we need separate Serializers/Views)?
2. Do we need to override the post method to apply the correct salt/hashing?

Thanks in advance.

Regards,

Carlton

Tom Christie

unread,
Nov 16, 2012, 12:42:50 PM11/16/12
to django-res...@googlegroups.com
Hi Carlton,

> 1. (How) can we allow the extra filed on POST (or do we need separate Serializers/Views)?

  For this use case you probably want to write the APIView yourself, without relying on the Generic Views.
All of the Generic views use the same serializer for validating the input, and serializing the output, which is fine in most cases, but doesn't quite work here.

  In your custom APIView, you'll probably want one serializer (which does include a password field) that you use for validating the input, and then a second for actually serializing the user (which doesn't).

  I'd suggest taking a dig around the existing create/update code as it's actually very simple.

> 2. Do we need to override the post method to apply the correct salt/hashing? 

  Because creating users is a little different to the usual case of creating a model instance due to the password saving, you probably want to look at overriding the 'restore_object` method on the serializer


I guess it'd be something like:

    def restore_object(self, attrs, instance=None):
    if instance:  #Update
        user = instance
        user.username = attrs['username']
        user.email = attrs['email']
else:
                user = User(username=attrs['username'], email=attrs['email'],
                is_staff=False, is_active=True, is_superuser=False)
   user.set_password(password)
   user.save()
return user

Hope that helps.  If you get it working okay, I'd really like to see what you end up with as I think this one is probably a reasonable common case for folks to bump into.

Carlton Gibson

unread,
Nov 16, 2012, 12:53:27 PM11/16/12
to django-res...@googlegroups.com
Hi Tom, 

Thanks for the quick reply — that gives me lots to play with over the weekend. :-)

On 16 Nov 2012, at 18:42, Tom Christie <christ...@gmail.com> wrote:

  I'd suggest taking a dig around the existing create/update code as it's actually very simple.


I've been digging around quite a lot — it all seems quite simple really — which is testament to how well thought through it is. 
(Thank you!)

  If you get it working okay, I'd really like to see what you end up with as I think this one is probably a reasonable common case for folks to bump into.

I'll come back with my solution. 
(The least I can do.)

Have a good weekend all! 

Regards,

Carlton

Tom Christie

unread,
Nov 16, 2012, 1:03:24 PM11/16/12
to django-res...@googlegroups.com
Oh another thought: you might consider simply using a different endpoint for setting the users password. That might work out more simply.


Carlton Gibson

unread,
Nov 21, 2012, 3:41:13 PM11/21/12
to django-res...@googlegroups.com
Hi all, 

On 16 Nov 2012, at 18:42, Tom Christie <christ...@gmail.com> wrote:

> 1. (How) can we allow the extra filed on POST (or do we need separate Serializers/Views)?

  For this use case you probably want to write the APIView yourself, without relying on the Generic Views.
All of the Generic views use the same serializer for validating the input, and serializing the output, which is fine in most cases, but doesn't quite work here.

  In your custom APIView, you'll probably want one serializer (which does include a password field) that you use for validating the input, and then a second for actually serializing the user (which doesn't).

  I'd suggest taking a dig around the existing create/update code as it's actually very simple.

Just following up on this: I haven't written it yet but it is on the list. In the meantime I think I have the opposite problem. 

I have a model with an "updated" field which is: 

    updated = models.DateTimeField(auto_now=True)

DRF is making me provide a (valid) value even though it will never be used. 

Here I want "Fewer Fields on POST".

_And_ I can see this sort of thing coming up all the time. 

(Just another case, I have a list endpoint that I want users to POST to but on admins GET.) It seems to me it would be good to be able to tweak the serializers somehow to still be able to use the generic views. 

(Am on it but am hoping to fire-up more/better brains than merely mine.)

Regards,

Carlton

Tom Christie

unread,
Nov 22, 2012, 8:00:53 AM11/22/12
to django-res...@googlegroups.com
It sounds like you're looking for the read_only=True argument unless I'm missing something.

Carlton Gibson

unread,
Nov 22, 2012, 8:13:12 AM11/22/12
to django-res...@googlegroups.com


On Thursday, 22 November 2012 14:00:53 UTC+1, Tom Christie wrote:
It sounds like you're looking for the read_only=True argument unless I'm missing something.

No, I want the model to be writable via the endpoint — I just don't want the `updated` field to be required (it's value is disregarded anyway). A slight aside: this is the behaviour you get in the Admin, i.e. the updated field wouldn't be shown. 

The general problem seems to be needing different serializers — different field lists — for different methods (plus, slightly different, perhaps allowing differences depending on request.user too). 

Your answer of writing the view by hand will no doubt work — and I need to do it that way several times to grok everything — but I can see this kind of thing coming up quite a lot. It would be nice to handle it with the generic views. 

(Perhaps we already can — but your original suggestion seemed to imply not.)

Anyhow, I just wanted to out the thought out there — mainly in case there's something obvious I've missed. I'm working on it currently so I will come back when I'm the other side of something that runs. 

Regards,

Carlton

Carlton Gibson

unread,
Nov 22, 2012, 8:21:48 AM11/22/12
to django-res...@googlegroups.com

On Thursday, 22 November 2012 14:00:53 UTC+1, Tom Christie wrote:
It sounds like you're looking for the read_only=True argument unless I'm missing something.

Aha! You meant the field argument. 


Thank you!


Rod Afshar

unread,
Dec 1, 2012, 6:50:14 PM12/1/12
to django-res...@googlegroups.com
Just wanted to share how I went about tackling this. I have a similar need, working with a model that has a `password_hash` field and accepts a `password` through the API. I ended up using the restore_object and convert_object methods of ModelSerializer to get it done and still rely on the generic views. I was hoping to do it in a way that doesn't lose a lot of the work that ModelSerializer does for you. Does this look like a decent implementation?

models.py

class Account(models.Model):
    email = models.EmailField(unique=True, db_index=True, max_length=255)
    password_hash = models.CharField(max_length=255)

serializers.py

class AccountSerializer(serializers.ModelSerializer):
    password = serializers.CharField(widget=forms.PasswordInput())
    password_hash = serializers.CharField(read_only=True)

    created = serializers.DateTimeField(read_only=True)
    updated = serializers.DateTimeField(read_only=True)

    class Meta:
        model = Account

    def convert_object(self, obj):
        """Remove password field when serializing an object"""
        del self.fields['password']

        return super(AccountSerializer, self).convert_object(obj)

    def restore_object(self, attrs, instance=None):
        """Hash password field and remove it when deserializing"""
        attrs['password_hash'] = make_password(attrs['password'])
        del attrs['password']

        return super(AccountSerializer, self).restore_object(attrs, instance=None)

Carlton Gibson

unread,
Dec 8, 2012, 4:35:31 PM12/8/12
to django-res...@googlegroups.com
Hi All, 

Sorry for the slow reply; I quickly went with the "second endpoint" approach which delayed taking on the real problem.


On Sunday, 2 December 2012 00:50:14 UTC+1, Rod Afshar wrote:
Just wanted to share how I went about tackling this. 

I've bashed a working solution together combining Tom and Rod's approach.

I've added a ?set_password query parameter to the UserList view that switches the serializer on POSTs:

class UserList(generics.ListCreateAPIView):
    """
    API endpoint that represents a list of users.
    """
    permission_classes = (permissions.IsAdminUserOrPostOnly,)
    model = User
    serializer_class = UserSerializer

    def post(self, request, *args, **kwargs):
        if request.QUERY_PARAMS.__contains__('set_password'):
            self.serializer_class = UserPasswordSerializer
        return self.create(request, *args, **kwargs)

(I'm not convinced by the whole query-string switch thing — it's more a work-in-progress sticker.)

The UserPasswordSerializer looks like this:

class UserPasswordSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email','password')

    def convert_object(self, obj):
        """Remove password field when serializing an object"""
        del self.fields['password']

        return super(UserPasswordSerializer, self).convert_object(obj)

    def restore_object(self, attrs, instance=None):
        if instance:  #Update
            user = instance
            user.username = attrs['username']
            user.email = attrs['email']
        else:
            user = User(username=attrs['username'], email=attrs['email'],
               is_staff=False, is_active=True, is_superuser=False)
        user.set_password(attrs['password'])
        # note I don't save() here. The view's create() does that.
        return user

As I say, it's all work in progress (and all entirely derivative) but it works with auth.User and (thus far) has the desired behaviour. I'll continue working on it.

Regards,

Carlton


Otto Yiu

unread,
Jan 26, 2013, 9:49:33 PM1/26/13
to django-res...@googlegroups.com
I'm still trying to find the best way to tackle this topic without creating separate serializers or endpoints. I tried Rod's way, which seems like a clean way in doing it. However, I get an exception when deleting the field from the serializer in convert_object.

    def convert_object(self, obj):
        """Remove password field when serializing an object"""
        del self.fields['password']


Exception Type:KeyError
Exception Value:
'password'
Exception Location:/home/bas/.virtualenv/backupcp-ws/lib/python2.7/site-packages/django/utils/datastructures.py in __delitem__, line 137


Carlton Gibson

unread,
Jan 28, 2013, 3:55:17 AM1/28/13
to django-res...@googlegroups.com
Maybe the user in question doesn't have a password set?

Try wrapping the del in a conditional:

        if 'password' in self.fields:
            del self.fields['password']

Regards,

Carlton

Neamar Tucote

unread,
Mar 18, 2013, 1:06:20 PM3/18/13
to django-res...@googlegroups.com
Hi,

Had the same problem, solved it by creating a special Field for password.
Hope this helps

 
HIDDEN_PASSWORD_STRING = '<hidden>'
 
class PasswordField(serializers.CharField):
    """Special field to update a password field."""
    widget = forms.widgets.PasswordInput
   
    def from_native(self, value):
        """Hash if new value sent, else retrieve current password"""
        from django.contrib.auth.hashers import make_password
        if value == HIDDEN_PASSWORD_STRING or value == '':
            return self.parent.object.password
        else:
            return make_password(value)

    def to_native(self, value):
        """Hide hashed-password in API display"""
        return HIDDEN_PASSWORD_STRING

julio....@ucsp.edu.pe

unread,
Apr 18, 2013, 2:14:09 AM4/18/13
to django-res...@googlegroups.com
I've been on this problem all day. I read this and other threads but didn't find something very practical. This is finally what I ended up doing:

http://pastebin.com/bkDBBR3b

Hopefully will be useful to someone else.

Robert Kajic

unread,
Sep 29, 2013, 9:03:48 PM9/29/13
to django-res...@googlegroups.com
I had the same problem. Here is how I solved it:

class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'url', 'username', 'password', 'first_name', 'last_name', 'email']

    def restore_object(self, attrs, instance=None):
        user = super(UserSerializer, self).restore_object(attrs, instance)
        user.set_password(attrs['password'])
        return user

    def to_native(self, obj):
        ret = super(UserSerializer, self).to_native(obj)
        del ret['password']
        return ret

On Friday, November 16, 2012 5:09:00 PM UTC+1, Carlton Gibson wrote:

Val Neekman

unread,
Mar 10, 2014, 11:46:49 PM3/10/14
to django-res...@googlegroups.com
Please note that set_password() does NOT save the object and since you have called the super first, your object is already saved with raw password.

Just simply use post_save() to save the password.

    def post_save(self, obj, created=False):
        """
        On creation, replace the raw password with a hashed version.
        """
        if created:
            obj.set_password(obj.password)
            obj.save()

As far as the serilizer goes, make sure 'password' is wirte_only_field.


class ProfileCreateSerializer(serializers.ModelSerializer):
    """
    Profile serializer for creating a user instance.
    """
    class Meta:
        model = User
        fields = ('id', 'email', 'password', 'first_name', 'last_name', )
        write_only_fields = ('password',)
Reply all
Reply to author
Forward
0 new messages