"Leave password blank if dont want to change" in a django admin field

Django is a complicated but useful python web-framework, comparing to light weight frameworks like Bottle, web.py. I recently switch from Bottle, making use of it's powerful admin site to build a Email Account Management system.

An AdminSite offer interfaces to manage databases. It includes traditional authentications, permissions, data display and POST saving mechanisms, etc. , which are nasty trifles if you try implement them from scratch using bottle or other things.

Now I can build a decent system in about 100 lines code, after many many document reading and code digging, I want to share the story how I solve the problem encountered while implementing the "Leave password blank if don't want to change it" requirement. As commonly seen in other applications when user try to update their profile.

Firstly the background. My data Model:

1
2
3
4
5
class Users(Model):
    account = models.CharField(max_length=128, unique = True)
    password = models.CharField(db_column = 'crypt',max_length=384, blank=True)
    name = models.CharField(max_length=768)
    #...

The password is stored hashed in the DB crypt column. If I just let the AdminSite pickup things like that, it just show the password value in the plain input text.

Firstly I tell django to show it as a password input widget.

1
2
3
4
5
6
7
8
class MailUsersForm(forms.ModelForm):
    class Meta:
        model = Users
        widgets = {
                'password': forms.PasswordInput(render_value = False),
        }
class MailUsersAdmin(admin.ModelAdmin):
    form = MailUsersForm

Ah, overriding the form in the ModelAdmin does the trick.

Now in the edit page, the Password field shows an empty input widget.

django-password-field.png

Now I need to hash the password before commit to the DB. I found the save_model method.

1
2
3
4
5
6
7
class MailUsersAdmin(admin.ModelAdmin):
    # ...
    def save_model(self, request, obj, form, change):
        new_psw = request.POST["password"]
        if len(new_psw):
            obj.password = obj._hash_password()
            obj.save()

This works, but buggy. The password field is defaultly blank. It will clean the field in the DB without processing. The problem is, by the time in the save_model method is triggered, the obj is already updated by the values from the POST request. There's no way to get the old password value here. All I can do without changing the old password, is to skip the obj.save(). But what if I only want to update other things?

Through out the documents, I found some other places to approch the Model instance.

Firstly the django.forms.ModelForm.save method. The module instance is returned from this method (then passed to the save_model), so I inspect the object with ipdb to see if the data is updated here:

1
2
3
4
5
6
class MailUsersForm(forms.ModelForm):
 
    def save(self, *args, **kw):
        import ipdb; ipdb.set_trace()
        obj = super(MailUsersForm, self).save(*args, **kw)
        return obj

Unfortunately Yes. The instanced obj from the derived save is already updated to the POST value.

And django signals, pre_save, all the same way. (The signal thing is even emmited after the forms.ModelForm.save)

Googled around and nothing suitble, I decided to find the right way myself.

There must be somewhere in django codes that sets the attributes with values from the POST requsts. So I add a hook in the Model.

1
2
3
4
5
6
7
8
class Users(Model):
    #....
    def __setattr__(self, name, value):
        if name == "password":
            if self.id == 370:
                import ipdb; ipdb.set_trace()
 
        return Model.__setattr__(self, name, value)

The if self.id thing is for filtering the specific page I submit the save in the browser. Now ipdb let me in debug mode.

ipdb> w
 
#................. Many Many unrelated things
 
  /usr/lib/python2.7/site-packages/django/forms/forms.py(272)full_clean()
    270         self._clean_fields()
    271         self._clean_form()
--> 272         self._post_clean()
    273         if self._errors:
    274             del self.cleaned_data
 
  /usr/lib/python2.7/site-packages/django/forms/models.py(309)_post_clean()
    307         opts = self._meta
    308         # Update the model instance with self.cleaned_data.
 
--> 309         self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
    310
    311         exclude = self._get_validation_exclusions()
 
  /usr/lib/python2.7/site-packages/django/forms/models.py(51)construct_instance()
     49             file_field_list.append(f)
     50         else:
---> 51             f.save_form_data(instance, cleaned_data[f.name])
     52
     53     for f in file_field_list:
 
  /usr/lib/python2.7/site-packages/django/db/models/fields/__init__.py(454)save_form_data()
    452
    453     def save_form_data(self, instance, data):
--> 454         setattr(instance, self.name, data)
#---------------^ this matters!
    455
    456     def formfield(self, form_class=forms.CharField, **kwargs):
 
> /home/boypt/Projects/maildbadmin/maildbadmin/models.py(55)__setattr__()
     53             if self.id == 370:
     54                 import ipdb; ipdb.set_trace()
---> 55                 print "set :", value
     56 #            
     57 #

From the call stack, the setattr call to the Model changes the data. I check django/db/models/fields/__init__.py source, find that this is a Field base class, so the obvious solution is to override this method.

1
2
3
4
5
6
7
8
9
10
11
class PasswordCharField(models.CharField):
    def save_form_data(self, instance, data):
        if data != u'':
            data = instance._hashed_pwd(data)
            setattr(instance, self.name, data)
 
class Users(Model):
    #...
    password = PasswordCharField(db_column = 'crypt', max_length=384, blank=True)
 
    #.....

OK, now everything's done here. The save_model overriding is no more needed, for the Model instance's password will not be set to empty at all when the user left the field blank when they submit.

Creative Commons License
This work, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

Tagged with: ,
Posted in Python, web技术
2 comments on “"Leave password blank if dont want to change" in a django admin field
  1. Ugo Meda says:

    Thank you so much for this !

  2. 团长 says:

    感谢楼主啊,真是好东东,学习收藏了~~

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Performance Optimization WordPress Plugins by W3 EDGE