I am trying to extend the Django rest framework (version 3.x.x) with gender, created_at, updated_at fields that are defined in a separate model called UserProfile. When I try to update the UserProfile model instance including the nested User model instance, it updates only the UserProfile instance (gender, updated_at fields). So basically what I want to achieve is to be able to update also the email, first_name and last_name fields from the User model
models.py:
class UserProfile(models.Model):
user = models.OneToOneField(User, primary_key = True, related_name = 'profile')
gender = models.CharField(choices = GENDERS, default = 2, max_length = 64)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(auto_now = True)
def __unicode__(self):
return self.user.username
#receiver(post_save, sender = User)
def create_profile_for_user(sender, instance = None, created = False, **kwargs):
if created:
UserProfile.objects.get_or_create(user = instance)
#receiver(pre_delete, sender = User)
def delete_profile_for_user(sender, instance = None, **kwargs):
if instance:
user_profile = UserProfile.objects.get(user = instance)
user_profile.delete()
serializers.py:
class UserProfileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source = 'pk', read_only = True)
username = serializers.CharField(source = 'user.username', read_only = True)
email = serializers.CharField(source = 'user.email')
first_name = serializers.CharField(source = 'user.first_name')
last_name = serializers.CharField(source = 'user.last_name')
class Meta:
model = UserProfile
fields = (
'id', 'username', 'email', 'first_name', 'last_name',
'created_at', 'updated_at', 'gender',
)
read_only_fields = ('created_at', 'updated_at',)
def update(self, instance, validated_data):
#user = User.objects.get(pk = instance.user.pk);
user = instance.user
user.email = validated_data.get('user.email', user.email)
user.first_name = validated_data.get('user.first_name', user.first_name)
user.last_name = validated_data.get('user.last_name', user.last_name)
user.save()
instance.gender = validated_data.get('gender', instance.gender)
instance.save()
return instance
def create(self, validated_data):
user_data = validated_data.pop('user')
user = User.objects.create(**user_data)
profile = UserProfile.objects.create(user = user, **validated_data)
return profile
views.py:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserProfileViewSet(viewsets.ModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
Here's how I did it:
def update(self, instance, validated_data):
# First, update the User
user_data = validated_data.pop('user', None)
for attr, value in user_data.items():
setattr(instance.user, attr, value)
# Then, update UserProfile
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Basically, I looked into the original code for serializers (here, line 800) and modified it so that it would fit my needs.
Good luck.
A slight modification to the above code replacing the None with {}
Because 'NoneType' object has no attribute 'items'
def update(self, instance, validated_data):
# First, update the User
user_data = validated_data.pop('user', {})
for attr, value in user_data.items():
setattr(instance.user, attr, value)
# Then, update UserProfile
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
There is another way that I am implementing which takes care of validation. I am implementing it for handling PATCH
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('email', 'first_name', 'last_name')
class ProfileSerializer(serializers.ModelSerializer):
email = serializers.CharField(source='user.email')
first_name = serializers.CharField(source='user.first_name')
last_name = serializers.CharField(source='user.last_name')
class Meta:
model = Profile
exclude = ('user',)
def update(self, instance, validated_data):
user_data = validated_data.pop('user', {})
user_serializer = UserSerializer(instance.user, data=user_data, partial=True)
user_serializer.is_valid(raise_exception=True)
user_serializer.update(instance.user, user_data)
super(ProfileSerializer, self).update(instance, validated_data)
return instance
Related
Currently, I'm having a problem when overriding a form field value on my (Django==4.0.3) django admin form.
The objective is :
I have a specific user table that I'm connecting to AWS Cognito. And when the admin creates a new user in django, the system must create a request to create a new Cognito user.
Once the Cognito user is created it generates a "sub" code, and then the sub should be saved in django
Code Follows
Model
class BuyerUser(BaseModel):
buyer = models.ForeignKey(
Buyer, on_delete=models.RESTRICT, related_name="%(class)s_buyer"
)
cognito_sub = models.CharField(max_length=50)
given_name = models.CharField(max_length=50)
family_name = models.CharField(max_length=50)
preferred_username = models.CharField(max_length=50)
email = models.EmailField(blank=False)
terms_conditions_accepted_datetime = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.preferred_username
admin
class BuyerUsers(admin.ModelAdmin):
list_display = ('id', 'buyer', 'given_name', 'family_name', 'preferred_username', 'available')
list_filter = ('buyer', 'available',)
list_display_links = ('id', 'preferred_username',)
search_fields = ('buyer__name', 'preferred_username', 'available')
list_per_page = 20
form = BuyerUserChangeForm
add_form = BuyerUserAddForm # It is not a native django field. I created this field and use it in get_form method.
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during foo creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
admin.site.register(BuyerUser, BuyerUsers)
and my forms
class BuyerUserAddForm(forms.ModelForm):
grupo = forms.CharField()
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, instance=None, use_required_attribute=None,
renderer=None):
super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, instance,
use_required_attribute, renderer)
self.cognito_sub = None
def save(self, commit=True):
grupo = self.cleaned_data.get('grupo', None)
self.given_name = self.cleaned_data.get('given_name', None)
self.family_name = self.cleaned_data.get('family_name', None)
self.preferred_username = self.cleaned_data.get('preferred_username', None)
self.email = self.cleaned_data.get('email', None)
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"]
)["Sub"]
self.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
class Meta:
model = BuyerUser
# fields = '__all__'
exclude = ['terms_conditions_accepted_datetime']
class BuyerUserChangeForm(forms.ModelForm):
class Meta:
model = BuyerUser
fields = '__all__'elf.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
class Meta:
model = BuyerUser
# fields = '__all__'
exclude = ['terms_conditions_accepted_datetime']
class BuyerUserChangeForm(forms.ModelForm):
class Meta:
model = BuyerUser
fields = '__all__'
create
Change
This cognito sub field should have its value override after cognito-user is created. as it should be happening in the following code
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"]
)["Sub"]
self.cognito_sub = sub
In fact, this cognito-user is being created and the sub is correct. the BIG PROBLEM is: this sub is not saved. It is getting only the value from the form.
I've tried to hide sub field using exclude = ['cognito_sub','terms_conditions_accepted_datetime']
but only happens to save a empty value.
You may ask why I use Forms instead of simply override model.Save() method
and the answer is: I need the grupo field, but this field must be persisted in DB. It only exists in Cognito.
You have to assign the value to form.instance instead of directly to the form itself.
class BuyerUserAddForm(forms.ModelForm):
grupo = forms.CharField()
# ...
def save(self, commit=True):
grupo = self.cleaned_data.get('grupo', None)
self.preferred_username = self.cleaned_data.get('preferred_username', None)
self.email = self.cleaned_data.get('email', None)
cognito = CognitoDriver()
sub = cognito.parse_user(
cognito.create_user(self.preferred_username, self.email)["User"])["Sub"]
self.instance.cognito_sub = sub
cognito.add_group(self.preferred_username, grupo)
return super(BuyerUserAddForm, self).save(commit=commit)
You might even want to disable the input field completly using the disabled attribute. https://docs.djangoproject.com/en/4.0/ref/forms/fields/#disabled
class BuyerUserAddForm(forms.ModelForm):
cognito_sub = forms.CharField(disabled=True)
# ...
I would like to extend Django's user model using a OneToOneField in my Django api project, but I'm getting a weird error. I'm hoping someone can help me out. Below is my code and the error message.
models.py:
class Profile(models.Model):
yearOfExperience = models.PositiveIntegerField(default=1)
profession = models.CharField(max_length=250, blank=True, null=True)
dp = models.URLField(blank=True, null=True)
qualification = models.CharField(max_length=255, blank=True, null=True)
phoneNumber = models.CharField(max_length=255, blank=True, null=True)
#receiver(post_save, sender=CustomUser)
def create_user_profile(sender, instance=None, created=False, **kwargs):
if created:
Profile.objects.create(user=instance)
instance.profile.save()
serializer:
class ProfileSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source = 'pk', read_only = True)
username = serializers.CharField(source = 'user.username', read_only = True)
email = serializers.CharField(source = 'user.email', read_only=True)
class Meta:
model = Profile
fields = ('id', 'email', 'username', 'yearOfExperience',
'qualification', 'profession', 'phoneNumber'
)
def create(self, validated_data, instance=None):
if 'user' in validated_data:
user_data = validated_data.pop('user')
user = CustomUser.objects.create(**validated_data)
profile = Profile.objects.update_or_create(user=user, **validated_data)
return user
apiView:
class ProfileListView(generics.ListCreateAPIView):
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Error message:
File "/home/olaneat/Desktop/filez/project/django/funzone/lib/python3.7/site-packages/django/db/models/base.py", line 500, in __init__
raise TypeError("%s() got an unexpected keyword argument '%s'" % (cls.__name__, kwarg))
TypeError: CustomUser() got an unexpected keyword argument 'yearOfExperience'
I believe you're missing the user one to one field to your profile model so
add it there
class Profile(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
...
Since you're using post_save for your profile, there's no need for the .create in your serializer. So something like this:
class ProfileSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField(read_only = True)
email = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Profile
fields = ('id', 'email', 'username', 'yearOfExperience',
'qualification', 'profession', 'phoneNumber'
)
def get_username(self, obj):
return obj.user.username
def get_email(self, obj):
return obj.user.email
now just post from the API view and the profile will be created.
If however, you want to keep the .create for the option to add new users from the profile API then perhaps something like this:
class ProfileSerializer(serializers.ModelSerializer):
username = serializers.SerializerMethodField(read_only = True)
email = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Profile
fields = ('id', 'email', 'username', 'yearOfExperience',
'qualification', 'profession', 'phoneNumber'
)
def create(self, validated_data, instance=None):
if 'user' in validated_data:
user = validated_data.pop('user')
else:
user = CustomUser.objects.create(**validated_data)
profile, created_profile = Profile.objects.update_or_create(user=user, **validated_data)
return profile
def get_username(self, obj):
return obj.user.username
def get_email(self, obj):
return obj.user.email
whenever is authenticated user post in My device I've to check id the device is associated with this user or not? for that, I've get all the information using request.user, but instead I am just getting an email address, is there any solution for this?
My custom User model
class User(AbstractUser):
"""User model."""
username = None
email = models.EmailField(_('email address'), unique=True)
mobile_token = models.CharField(max_length=20,blank=True,null=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserManager_1()
My device model
class Device(models.Model):
# user_id = models.CharField(max_length=20,primary)
device_id = models.CharField(max_length=20 ,primary_key=True )
email = models.ForeignKey(User,on_delete=models.CASCADE,blank = True,null=True)
date_added = models.DateField(default=timezone.now)
def __str__(self):
return self.device_id
My serailzers
class UserSerializer(serializers.ModelSerializer):
# my_field = serializers.SerializerMethodField()
# def get_field_name(self, obj):
# return "name"
class Meta:
model = models.User
# key= {'status':'success'}
fields = ('first_name', 'email','last_name','mobile_token',)
class DeviceSerializer(serializers.ModelSerializer):
# locations = serializers.PrimaryKeyRelatedField(many=True,
queryset=models.User.objects.all())
class Meta:
model = models.Device
fields = ('date_added','email','device_id',)
And The View for device
class DeviceView(viewsets.ViewSet):
queryset = models.Device.objects.all()
serializer_class = serializers.DeviceSerializer
def list(self,request):
queryset = models.Device.objects.all()
serializer = serializers.DeviceSerializer(queryset, many=True)
final_data = serializer.data
import json
api={}
api['data'] = final_data
api['message'] = "Device ids"
# Device_name_api = final_data[]
# validators = serializer.get_validators()
api['status'] = 1
# print(request.data)
return Response(api)
def create(self, request, *args, **kwargs):
# response = super().post(request, *args, **kwargs)
# print(self.response)
serializer = self.serializer_class(data=request.data)
print(request.user)
# user_data=models.User.objects.all()
# print(user_data)
# print(request.META['HTTP_X_MYHEADER'])
# print(request.data['device_id'])
if serializer.is_valid():
return Response({
'data':serializer.data,
'status': 1,
'message': 'Succuess'
}, status=status.HTTP_400_BAD_REQUEST)
Try to use depth option:
class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = models.Device
fields = ('date_added','email','device_id',)
depth = 1
I have a model in my django project :
class Employee(models.Model):
user = models.OneToOneField(User, related_name="EmployeeUser")
workType = models.CharField(max_length=1)
salary = models.IntegerField()
Branch = models.ForeignKey(Branch)
is_admin = models.BooleanField(default=False)
and in forms.py
class EmpForm(ModelForm):
username = fields_for_model(User)['username']
email = fields_for_model(User)['email']
password = fields_for_model(User)['password']
first_name = fields_for_model(User)['first_name']
last_name = fields_for_model(User)['last_name']
branch = fields_for_model(Employee)['Branch']
is_admin = fields_for_model(Employee)['is_admin']
salary = fields_for_model(Employee)['salary']
def __init__(self, *args, **kwargs):
super(EmpForm, self).__init__(*args, **kwargs)
class Meta:
model = Employee
fields = []
def save(self, commit=True):
username = self.cleaned_data.get('username', None)
email = self.cleaned_data.get('email', None)
password = self.cleaned_data.get('password', None)
first_name = self.cleaned_data.get('first_name', None)
last_name = self.cleaned_data.get('last_name', None)
salary = self.cleaned_data.get('salary', 0)
branch = self.cleaned_data.get('branch', 0)
tmp_user = User.objects.create_user(username=username, email=email, password=password, first_name=first_name,
last_name=last_name)
return Employee.objects.create(user=tmp_user, salary=0, Branch=branch, workType='a', is_admin=False)
and in my admin.py:
class EmpAdmin(admin.ModelAdmin):
....
form=EmpFrom
def save_model(self, request, obj, form, change):
if form.is_valid():
obj.save()
The problem is that in django admin form when I want to enter a new Employee It throws an form exception that the salary field cannot be null but I have gave value to it.
[SOLEVED]
I just changed the :
class Meta:
model = Employee
fields = []
to
class Meta:
model = Employee
fields = ['first_name', 'last_name', 'username', 'password', 'branch', 'salary', 'is_admin']
and it worked
I want access User model's first_name, last_name and email fields from UserProfile as if they were UserProfile's own fields.
class Person(models.Model):
user = models.OneToOneField(User)
name = #ModelField which refers to user.first_name
surname = #ModelField which refers to user.last_name
email = #ModelField which refers to user.email
birth_date = models.DateField(null=True, blank=True)
#and other fields
I could use independent name, surname, email, fields but it would have caused data duplication with User fields.
upd
I want name, surname and email to be of some Field type (as birth_date) and modification of these fields to equal modification of corresponding User fields (property behavior shown below). I need this because I want these three fields to be edible in admin interface or to be prcocessed equally with "native" fields in ModelForm for example.
upd solution. Not very fancy, but here how it worked for me:
class Person(models.Model):
user = models.OneToOneField(User)
#and also some char, text fields
#property
def name(self):
return self.user.first_name
#name.setter
def name(self, value):
self.user.first_name = value
#and by analogy surname and email properties
class PersonForm(ModelForm):
class Meta:
model = Person
exclude = ('user',)
name = forms.CharField(max_length=100, required=False)
surname = forms.CharField(max_length=100, required=False)
email = forms.EmailField(required=False)
#no make fields filled with User data on form load
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
instance = kwargs['instance']
initial = kwargs.get('initial', {})
initial['name'] = instance.name
initial['surname'] = instance.surname
initial['email'] = instance.email
kwargs['initial'] = initial
super(PersonForm, self).__init__(*args, **kwargs)
# to save form data to User model when commit
def save(self, commit=True):
instance = super(PersonForm, self).save(commit)
user = instance.user
user.first_name = self.cleaned_data['name']
user.last_name = self.cleaned_data['surname']
user.email = self.cleaned_data['email']
user.save()
return instance
class PersonAdmin(admin.ModelAdmin):
fields = ['name', 'surname', 'email', 'and_others']
form = PersonForm
admin.site.register(Person, PersonAdmin)
If want you want is to access user.first_name, user.last_name, etc... directly as attributes of Person, you can add a name, surname, etc... properties to Person model, instead of fields:
class Person(models.Model):
user = models.OneToOneField(User)
birth_date = models.DateField(null=True, blank=True)
#and other fields
#property
def name(self):
return self.user.first_name
#property
def surname(self):
return self.user.last_name
....
class Person(models.Model):
user = models.OneToOneField(User)
#property
def name(self):
return self.user.first_name
#property
def surname(self):
return self.user.last_name