I have a problem similar to this one: Additional (non model) fields in ModelSerializer
I want to create a object with a modelserializer like this:
class CreateUserSerializer(serializers.ModelSerializer):
user = serializers.CharField(source="username")
password = serializers.CharField()
password2 = serializers.WritableField()
...
class Meta:
model = User
fields = ('user', "password", "password2", ...)
Obviously, I am not interested in saving password2, it's just for the reason of comparison.
Ideally this should do the trick but for reasons, its not working as expected:
def restore_object(self, attrs, instance=None):
tags = attrs.pop('password2', None)
obj = super(CreateUserSerializer, self).restore_object(attrs, instance)
obj.tags = tags
logging.info(u"Object gets restored, field 'password2' gets removed, list with arguments for object: {0}".format(tags))
return obj
The Error I am getting is:
'User' object has no attribute 'password2'
...
/home/jan/projekte/alarmapp/eclipse_workspace/AlarmServer/AlarmApp/src/external/rest_framework/fields.py in get_component
55. val = getattr(obj, attr_name)
Any Idea why this isn't working?
Seems like this small Gist should be helpful:
from django.contrib.auth.models import User
from rest_framework import serializers
class CreateUserSerializer(serializers.ModelSerializer):
password2 = serializers.CharField()
def validate_password2(self, attrs, source):
password2 = attrs.pop(source)
if attrs['password'] != password2:
raise serializers.ValidationError('password mismatch')
return attrs
def to_native(self, obj):
self.fields.pop('password2')
return super(CreateUserSerializer, self).to_native(obj)
class Meta:
model = User
Related
I am trying to make a registration key in the UserModel where the key field in the registration form is a foreign key to another model called RegistrationKey. I have made two posts about this topic earlier without any success, however has a few things changed in my code which makes those previous posts irrelevant. In the form field, the field for the key is a CharField as I can not display the keys for the users due to safety.
These are the two previous posts:
Save user input which is a string as object in db ,
Textinput with ModelChoiceField
These are my two models.
class RegistrationKey(models.Model):
key = models.CharField(max_length=30)
def __str__(self):
return self.key
class User(AbstractUser):
key = models.ForeignKey(RegistrationKey, on_delete=models.CASCADE, null=True, blank=True)
Since my latest posts have I created a class based view, which looks like this:
class RegisterPage(CreateView):
form_class = MyUserCreationForm
def form_valid(self, form):
key = form.cleaned_data['key']
try:
keyobject = RegistrationKey.objects.get(key=key)
form.instance.key = keyobject
return super().form_valid(form)
except RegistrationKey.DoesNotExist:
form.add_error('key', 'error')
return super().form_invalid(form)
When I try and pass in the value Admin which is an object in the RegistrationKey model I get the following error:
'Cannot assign "'Admin'": "User.key" must be a "RegistrationKey" instance.'
I don't know how to solve this, how can this string that the user inputs be assigned to the db?
Edit
Here are my form
class MyUserCreationForm(UserCreationForm):
key = forms.CharField(widget=forms.TextInput(attrs={'class':'form-control', 'placeholder':'Key'}), label='')
email = forms.EmailField(widget=forms.EmailInput(attrs={'class':'form-control', 'placeholder':'Email'}), label='')
class Meta:
model = User
fields = ('key', 'email', 'password1', 'password2')
def __init__(self, *args, **kwargs):
super(MyUserCreationForm, self).__init__(*args, **kwargs)
self.fields['password1'].widget.attrs['class'] = 'form-control'
self.fields['password1'].widget.attrs['placeholder'] = 'Password'
self.fields['password1'].label=''
self.fields['password2'].widget.attrs['class'] = 'form-control'
self.fields['password2'].widget.attrs['placeholder'] = 'Confirm Password'
self.fields['password2'].label=''
for fieldname in ['password1', 'password2']:
self.fields[fieldname].help_text = None
You better move the logic to obtain the item to the form, where it belongs. So with:
from django.core.exceptions import ValidationError
class MyUserCreationForm(UserCreationForm):
# …
def clean_key(self):
key = self.cleaned_data['key']
try:
return RegistrationKey.objects.get(key=key)
except RegistrationKey.DoesNotExist:
raise ValidationError('The key is not valid.')
That should be sufficient. You should not override the .form_valid(…) method.
I'm using Python 3.7, Django 2.2, the Django rest framework, and pytest. I have the following model, in which I want to re-use an existing model if it exists by its unique key ...
class CoopTypeManager(models.Manager):
def get_by_natural_key(self, name):
return self.get_or_create(name=name)[0]
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False, unique=True)
objects = CoopTypeManager()
Then I have created the below serializer to generate this model from REST data
class CoopTypeSerializer(serializers.ModelSerializer):
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
However, when I run the below test in which I intentionally use a name that is taken
#pytest.mark.django_db
def test_coop_type_create_with_existing(self):
""" Test coop type serizlizer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
print(serializer.errors)
assert serializer.is_valid(), serializer.errors
result = serializer.save()
assert result.name == name
I get the below error
python manage.py test --settings=directory.test_settings
... ----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/davea/Documents/workspace/chicommons/maps/web/tests/test_serializers.py", line 46, in test_coop_type_create_with_existing
assert serializer.is_valid(), serializer.errors
AssertionError: {'name': [ErrorDetail(string='coop type with this name already exists.', code='unique')]}
How do I construct my serializer so that I can create my model if its unique key doesn't exist, or re-use it if it does?
Edit: Here's the GitHub link ...
https://github.com/chicommons/maps/tree/master/web
DRF validates the uniqueness of each field if is declared with unique=True in the model, so you have to change the model as following if you want to keep your unique contraint for the name field:
class CoopType(models.Model):
name = models.CharField(max_length=200, null=False)
objects = CoopTypeManager()
class Meta:
# Creates a new unique constraint with the `name` field
constraints = [models.UniqueConstraint(fields=['name'], name='coop_type_unq')]
Also, you have to change your serializer, if you're using a ViewSet with the default behavior, you only need to add a custom validation in the serializer.
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import CoopType
class CoopTypeSerializer(serializers.ModelSerializer):
default_error_messages = {'name_exists': 'The name already exists'}
class Meta:
model = CoopType
fields = ['id', 'name']
def validate(self, attrs):
validated_attrs = super().validate(attrs)
errors = {}
# check if the new `name` doesn't exist for other db record, this is only for updates
if (
self.instance # the instance to be updated
and 'name' in validated_attrs # if name is in the attributes
and self.instance.name != validated_attrs['name'] # if the name is updated
):
if (
CoopType.objects.filter(name=validated_attrs['name'])
.exclude(id=self.instance.id)
.exists()
):
errors['name'] = self.error_messages['name_exists']
if errors:
raise ValidationError(errors)
return validated_attrs
def create(self, validated_data):
# get_or_create returns a tuple with (instance, boolean). The boolean is True if a new instance was created and False otherwise
return CoopType.objects.get_or_create(**validated_data)[0]
The update method was removed because is not needed.
Finally, the tests:
class FactoryTest(TestCase):
def test_coop_type_create_with_existing(self):
""" Test coop type serializer model if there is already a coop type by that name """
coop_type = CoopTypeFactory()
serializer_data = {
"name": coop_type.name,
}
# Creation
serializer = CoopTypeSerializer(data=serializer_data)
serializer.is_valid()
self.assertTrue(serializer.is_valid(), serializer.errors)
result = serializer.save()
assert result.name == serializer_data['name']
# update with no changes
serializer = CoopTypeSerializer(coop_type, data=serializer_data)
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
# update with the name changed
serializer = CoopTypeSerializer(coop_type, data={'name': 'testname'})
serializer.is_valid()
serializer.save()
self.assertTrue(serializer.is_valid(), serializer.errors)
coop_type.refresh_from_db()
self.assertEqual(coop_type.name, 'testname')
When you are using unique=True key in model, Serializer will automaticly add unique validator to that field.
It’s enough to cancel the uniqueness check by writting your own name field directly in serializer to prevent your curent error:
class Ser(serializers.ModelSerializer):
name = serializers.CharField() # no unique validation here
class Meta:
model = CoopType
fields = ['id', 'name']
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
Be carefull: get_or_create in create method will return tuple, not instance.
Ok, now imagine you will call it with id field too so you really need an update method.
Then you can make the following hack in validate method (maybe it's dirty, but it will work):
class Ser(serializers.ModelSerializer):
# no `read_only` option (default for primary keys in `ModelSerializer`)
id = serializers.IntegerField(required=False)
# no unique validators in charfield
name = serializers.CharField()
class Meta:
model = CoopType
fields = ["id", "name"]
def validate(self, attrs):
attrs = super().validate(attrs)
if "id" in attrs:
try:
self.instance = CoopType.objects.get(name=attrs["name"])
except CoopType.DoesNotExist:
pass
# to prevent manual changing ids in database
del attrs["id"]
return attrs
def create(self, validated_data):
return CoopType.objects.get_or_create(**validated_data)
def update(self, instance, validated_data):
# you can delete that method, it will be called anyway from parent class
return super().update(instance, validated_data)
The save method on the serializer checks if the field self.instance is null or not. If there is an non-empty self.instance, it will call the update method; else - create method.
So if CoopType with name from your serializer_data dictionary exists - update method will be called. In other case you will see create method call.
My suggestion is to not use a ModelSerializer but instead use a vanilla serializer.
class CoopTypeSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=200, required=True, allow_blank=False)
def create(self, validated_data):
"""
Create and return a new `CoopType` instance, given the validated data.
"""
return CoopType.objects.get_or_create(**validated_data)[0]
def update(self, instance, validated_data):
"""
Update and return an existing `CoopType` instance, given the validated data.
"""
instance.name = validated_data.get('name', instance.name)
instance.save()
return instance
I have created custom Userclass in django(AbstarctUser). Everything works fine but my password is getting stored as plain text in database even after registering in admin.py. I do not have any forms.py explicitly defined.
Also I am using nested serializers following tutorial.
My code is as below
from django.contrib import admin
from .models import BasicUserInfo
from django.contrib.auth.admin import UserAdmin
class BasicUserAdmin(UserAdmin):
pass
admin.site.register(BasicUserInfo, BasicUserAdmin)
Edited to add Models and views
Models.py
class BasicUserInfo(AbstractUser):
email = models.EmailField(primary_key=True, unique=True, db_index=True)
class UserInfo(models.Model):
user = models.ForeignKey(BasicUserInfo, on_delete=models.CASCADE)
Views.py
serializer = AddUserSerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
Serializers.py
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
fields = ('username', 'password', 'email')
print("hete")
def create(self, validated_data):
retval = BasicUserInfo.objects.create(**validated_data)
password = validated_data.pop('password')
self.password = make_password(password)
# self._password = password
return retval
class AddUserSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(required=True)
class Meta:
model = UserInfo
fields = ('phoneNo')
def create(self, validated_data):
user_data = validated_data.pop('user')
user = BasicUserSerializer.create(BasicUserSerializer(), validated_data=user_data)
user_info, created = UserInfo.objects.update_or_create(user=user, phoneNo=validated_data.pop('phoneNo'))
return user_info
The trick is to use user.set_password(password) -> this internally triggers the password hashing mechanism: Here's the Django code that does this:
def set_password(self, raw_password):
self.password = make_password(raw_password)
self._password = raw_password
def make_password(password, salt=None, hasher='default'):
"""
Turn a plain-text password into a hash for database storage
Same as encode() but generate a new random salt. If password is None then
return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
which disallows logins. Additional random string reduces chances of gaining
access to staff or superuser accounts. See ticket #20079 for more info.
"""
if password is None:
return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher)
salt = salt or hasher.salt()
return hasher.encode(password, salt)
So the problem is serializers.create(**validated_data) is not performing the make_password operation. The above answer works perfectly fine, except it does two things differently
- It saves the user twice (once in serailizer.create and again during `user.save())
- It does not hande everything within the serializer, part of the work is being split b/w the serializer and the view.
If you want to keep it all within the serializer, you can do the following:
class AddUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
def validate_password(self, value):
return make_password(value)
Update:
I've made a bunch of edits; and tried to explain why. Please read patiently, and incorporate changes as you see fit.
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = BasicUserInfo
fields = ('username', 'password', 'email')
def validate_password(self, value):
return make_password(value)
class AddUserSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(required=True)
class Meta:
model = UserInfo
fields = ('phoneNo')
def create(self, validated_data):
user_data = validated_data.pop('user')
user_serializer = BasicUserSerializer(data=user_data)
if user_serializer.is_valid(raise_exception=True):
user = user_serializer.save()
validated_data['user'] = user
return UserInfo.objects.create(**validated_data)
You should not use like this:
serializer = AddUserSerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
if password in validated data
it is better to use like this:
password = request.data.pop('password', '')
if not password:
raise ValidationError('password must not be empty')
serializer = AddUserSerializer(data=request.data)
serializer.is_valid(raise_exception=ValueError):
user = serializer.create(validated_data=request.data)
user.set_password(password)
user.save()
Try to overwrite User models by the following code, but somehow I cannot overwrite the max_length and min_length of username.
More specifically, when I check by python manage.py shell, I do overwrite them. But it seems has no effect on the html which was rendered(username maxlength is still 150).
Don't know which parts get wrong, please help.
from django import forms
from django.contrib.auth.models import User
class RegistrationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(RegistrationForm, self).__init__(*args, **kwargs)
email = self.fields['email']
username = self.fields['username']
email.required = True
email.label_suffix = ' '
username.label_suffix = ' '
######### this is not work!!!###############
username.min_length = 6
username.max_length = 30
############################################
class Meta:
model = User
fields = ('username', 'email')
labels = {
'username': '帳號',
}
help_texts = {
'username': '',
}
Instead of modifying the form, you should modify/override the model.
I recommend using django-auth-tools for building your own custom user model. It supplies basic models, views and forms which can be easily extended.
If you are trying to override just the model form field, you could do something like this
class RegistrationForm(forms.ModelForm):
username = forms.CharField(required=True, min_length=6, max_length=30)
class Meta:
model = User
fields = ('username', 'email')
or
class RegistrationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(RegistrationForm, self).__init__(*args, **kwargs)
self.fields['username'] = forms.CharField(required=True, min_length=6, max_length=30)
class Meta:
model = User
fields = ('username', 'email')
But I would recommend creating a Custom User Model inherited from AbstractBaseUser to override the username or email field as documented in https://docs.djangoproject.com/en/1.10/topics/auth/customizing/
serializers.py:
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
import models
class ExtensibleModelSerializerOptions(serializers.Serializer):
def __init__(self, meta):
super(ExtensibleModelSerializerOptions, self).__init__(meta)
self.model = getattr(meta, 'model', None)
self.read_only_fields = getattr(meta, 'read_only_fields', ())
self.non_native_fields = getattr(meta, 'non_native_fields',())
class ExtensibleModelSerializer(serializers.ModelSerializerOptions):
_options_class = ExtensibleModelSerializerOptions
def restore_object(self, attrs, instance=None):
for field in self.opts.non_native_fields:
attrs.pop(field)
return super(ExtensibleModelSerializer, self).restore_object(attrs, instance)
def to_native(self, obj):
ret = self._dict_class()
ret.fields = {}
for field_name, field in self.fields.items():
# --- BEGIN EDIT --- #
if field_name in self.opts.non_native_fields:
continue
# --- END --- #
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
ret.fields[key] = field
return ret
class OptiUserSerializer(ExtensibleModelSerializer):
password_confirmation = serializers.CharField(max_length=models.OptiUser._meta.get_field('password').max_length)
company_id = serializers.PrimaryKeyRelatedField(queryset=models.Company.objects.all(), many=False)
password = serializers.CharField(
style={'input_type': 'password'}
)
def validate_password_confirmation(self, attrs, source):
password_confirmation = attrs[source]
password = attrs['password']
if password_confirmation != password:
raise serializers.ValidationError(_('Password confirmation mismatch'))
return attrs
class Meta:
model = models.OptiUser
fields = ('id', 'username', 'company_id', 'first_name', 'last_name', 'is_active', 'is_admin', 'password', 'password_confirmation',)
non_native_fields = ('password_confirmation',)
I'm having a customized User model for my app. I'm creating web a web service using DRF. I'm trying to make a user registration serializer as specified above. Everything is working fine except when I add 'password_confirmation' field it raises exception "model_name" object has no attribute password_confirmation". I found a code snippet as above which I'm trying to use above. But unfortunately it doesn't seems to be much helpful.
My question is that, Is it possible to add such a field and perform some comparisons on it. Is there any elegant way to do it?
Please suggest a fix.
Thanks! in Advance.
I don't think you need ExtensibleModelSerializer. Set the field to write_only:
password_confirmation = serializers.CharField(write_only=True, ...)