I'm working on a User Preferences viewset in DJANGO REST API in which a user can get a list of preferences along with updating the preferences. In Postman i can get the user's preferences, but when i go to 'put' i'm getting the following error: Integrity Error --NOT NULL constraint failed: pugorugh_userpref.age --any reason this might be happening?
The UserPref Model is below:
class UserPref(models.Model):
user = models.ForeignKey(User)
age = models.CharField(choices=AGE, max_length=7, default='b,y,a,s')
gender = models.CharField(choices=GENDER_PREF, max_length=3, default='m,f')
size = models.CharField(choices=SIZE_PREF, max_length=8, default='s,m,l,xl')
def __str__(self):
return '{} preferences'.format(self.user)
def create_user_preference(sender, **kwargs):
user = kwargs['instance']
if kwargs['created']:
user_pref = UserPref(user=user)
user_pref.save()
post_save.connect(create_user_preference, sender=User)
Here is my ViewSet:
class UserPrefViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
"""View to get and update User Preferences."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.UserPref.objects.all()
serializer_class = serializers.UserPrefSerializer
# /api/user/preferences/
#list_route(methods=['get', 'put'])
def preferences(self, request, pk=None):
user = request.user
user_pref = models.UserPref.objects.get(user=user)
if request.method == 'PUT':
data = request.data
user_pref.age = data.get('age')
user_pref.gender = data.get('gender')
user_pref.size = data.get('size')
user_pref.save()
serializer = serializers.UserPrefSerializer(user_pref)
return Response(serializer.data)
and SERIALIZER
class UserPrefSerializer(serializers.ModelSerializer):
extra_kwargs = {
'user': {'write_only': True}
}
class Meta:
fields = (
'age',
'gender',
'size'
)
model = models.UserPref
Looks like PUT data doesnt contain age value. Since age field is not nullable, blank age value raise error.
Try to fix this:
user_pref.age = data.get('age') or user_pref.age
...
user_pref.save()
this allows not to change age if value not in request data.
Related
I am using DRF to create an API in a single page application.
I have a customer user class to which I have only added a is_manager flag and a managerEntity model where users that have the is_manager flag as True can create managerEntities becoming owners of them.
The problem is that I can't seem to figure out how to validate the data from the serializer before create method to check whether the is_manager is set or not. If set, the managerEntity should be created, if not, raise an exception.
class DeepmetricsUser(AbstractUser):
is_manager = models.BooleanField(default=False)
class managerEntity(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=200)
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
team = models.ManyToManyField(get_user_model(), blank=True)
views.py
class managersEntityViewSet(viewsets.ModelViewSet):
queryset = managerEntity.objects.all()
serializer_class = managerEntityModelSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return self.queryset.filter(Q(owner = self.request.user) | Q(team=self.request.user.id))
def create(self, request, *args, **kwargs):
serializer = managerEntitySerializer(data=request.data, context={"request": self.request})
serializer.is_valid(raise_exception=True)
res = serializer.save()
data = managerEntityModelSerializer(res).data
return Response(data, status=status.HTTP_201_CREATED)
serializer.py
class managerEntitySerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
def create(self, data):
res = managerEntity.objects.create(**data)
return res
You need to override the validate method in Serializer
def validate(self, attrs):
if not self.context["request"].user.is_manager:
raise serializers.ValidationError("Validation error")
return attrs
I found a solution that fits better my needs by using permissions. The answer provided by Shakeel is correct as I asked for validation and that should be done as he suggested but, what I really wanted to do was checking for enough clearance for the user to manipulate a resource, then, permissions is what's best fits:
class createManagerEntity(BasePermission):
message = "Not enough privilegies"
def has_permission(self, request, view):
return request.user.is_manager
I have Contact model to list the followers of an User object, I try to filter the contacts of a User but I still could not manage get a correct queryset. My Contact model is simple with two ForeignKey:
class Contact(models.Model):
user_from = models.ForeignKey(User,related_name='rel_from_set', on_delete=models.CASCADE,)
user_to = models.ForeignKey(User,related_name='rel_to_set', on_delete=models.CASCADE,)
def __str__(self):
return '{} follow {}'.format(self.user_from, self.user_to)
I have created serializers for User and Contact:
##Contact Serializer
class ContactsSerializer(serializers.ModelSerializer):
user_from = serializers.StringRelatedField(read_only=True)
user_to = serializers.StringRelatedField(read_only=True)
class Meta:
model = Contact
fields = ["user_from", "user_to"]
##UserSerializer
class UserInformationSerializer(serializers.ModelSerializer):
followers = ContactsSerializer(many=True, read_only=True)
class Meta:
model = User
fields = ['first_name', 'last_name', 'followers']
And try to make a query through views:
class FollowerListView(APIView):
queryset = Contact.objects.all()
serializer_class = ContactsSerializer
lookup_field = "username"
def get(self, request, format=None, slug=None):
kwarg_username = self.kwargs.get("slug")
user = User.objects.filter(is_active=1).filter(username=kwarg_username)
print(user.username)
contacts = Contact.objects.filter(user_to=user.id)
serializer = ContactsSerializer(contacts)
return Response(serializer.data)
Now I get error message:
AttributeError at /api/member/ytsejam/followers/
'QuerySet' object has no attribute 'username'
print(user.username)
If i try print(user) I can see the user an Object.
Can you guide me how to correct?
Thanks
filter will always return a queryset. If you expect to retrieve one single item, use get.
So that it looks like that:
def get(self, request, format=None, slug=None):
kwarg_username = self.kwargs.get("slug")
user = User.objects.filter(is_active=1).get(username=kwarg_username)
print(user.username)
contacts = Contact.objects.filter(user_to=user.id)
serializer = ContactsSerializer(contacts)
return Response(serializer.data)
You could, of course, do this on one take:
User.objects.get(is_active=1, username=kwarg_username)
But beware, if there are two rows in your model that would satisfy this call, Django will throw an error. Best make sure that the username has a unique constraint.
I'm trying to limit the fields list in serializers based on user permissions. I have a generic routine that does it for all serializers. It's working on the parent serializer, but not on a nested serializer.
I have a client model, and a client profile (referred to as "contacts") as shown below. The client profile model is an extension of the user model (one-to-one relationship).
class Client(AddressPhoneModelMixin, DateFieldsModelMixin, models.Model):
name = models.CharField(max_length=100)
status = models.CharField(max_length=25)
class Meta:
permissions = (
# Object-level
('view_all_clients', 'Can view all clients'),
('change_all_clients', 'Can change all clients'),
# Field-level
('view_client_id', 'Can view client ID'),
('view_client_name', 'Can view client name'),
...others omitted...
)
class ClientProfile(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
blank=True,
null=True,
)
client = models.ForeignKey(
Client,
on_delete=models.PROTECT,
related_name='contacts',
)
receive_invoices = models.BooleanField(default=False)
My object-level permission logic is in the list view:
class ClientList(ListAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ClientSerializer
def get_queryset(self):
user = self.request.user
queryset = None
if user.has_perm('view_client') or user.has_perm('clients.view_all_clients'):
queryset = Client.objects.all().exclude(status__in=['deleted', 'archived'])
if user.has_perm('view_client'): # View only "assigned" clients
if user.type == 'client':
# See if user is a "contact".
queryset = queryset.distinct().filter(contacts__user=self.request.user)
else:
# See if user is assigned to projects for the client(s).
queryset = queryset.distinct().filter(projects__project_users__user=self.request.user)
if queryset is None:
raise PermissionDenied('You do not have permission to view clients.')
return self.get_serializer_class().setup_eager_loading(queryset)
Removing fields from the serializer "fields" property is done in the serializer __init__ method (from examples I found here in SO):
class ClientContactsSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:clientprofile-detail')
user = UserSerializer()
class Meta:
model = ClientProfile
fields = (
'url',
'receive_invoices',
'user',
)
def __init__(self, *args, **kwargs):
super(ClientContactsSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
class ClientSerializer(AddressPhoneSerializerMixin, serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:client-detail')
contacts = ClientContactsSerializer(many=True, read_only=True)
projects = ClientProjectsSerializer(many=True, read_only=True)
class Meta:
model = Client
fields = (
'url',
'id',
'name',
...omitted for brevity...
'contacts',
'projects',
)
def __init__(self, *args, **kwargs):
super(ClientSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('country')
return queryset.prefetch_related('contacts', 'contacts__user', 'contacts__user__country', 'projects')
And, finally, here's the check_field_permissions function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
for field_name in fields:
if hasattr(serializer.fields[field_name], 'child'):
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
Stepping through in debug on a page-load of the Client list, I can see that the above function is invoked first for clientprofile, and request is None. The second time, it is invoked for client, and request is a valid request object.
First question, is __init__ the correct place for limiting the list of fields to be serialized?
Second, how to get the request object in the nested serializer (clientprofile)?
After reading a post by Tom Christie, who is an undisputed authority on DRF, I was able to solve my issue. He pointed out that each nested serializer does, in fact, have the context object (and the request, and the user). You just have to deal with nested serializers in the parent __init__ - not their own __init__.
Here's my revised check_field_permissions() function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
extra_fields = []
for field_name in fields:
if field_name == 'url':
extra_fields.append(field_name)
continue
if hasattr(serializer.fields[field_name], 'child'):
check_field_permissions(serializer.fields[field_name].child, action)
extra_fields.append(field_name)
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
# If only "url" and child fields remain, remove all fields.
if len(serializer.fields) == len(extra_fields):
for field_name in extra_fields:
serializer.fields.pop(field_name)
It is now recursive. If it hits a field with a "child" attribute, it knows that's a nested serializer field. It calls itself with that child serializer passed as an arg.
The other change is that __init__ was removed from ClientContactsSerializer, because it doesn't need to call check_field_permissions().
I have a serializer that works fine for the GET, POST, DELETE actions. It exposes the model fields that I want. However for the PUT action, the user will send back values that aren't built into my models and the server will deal with how to perform the update on the model. I can send the data back using Postman or Curl and it works but the browseable API still looks like this:
For the PUT method I want "is_winner", "num_hands_won", and "score" to show up instead of the actual model fields. How do I do this? (Let me know in the comments if you need more info)
StatisticsSerializer:
class StatisticsSerializer(serializers.ModelSerializer):
# pk = serializers.IntegerField(required=False)
class Meta:
model = Statistics
fields = [
'url',
'games_won',
'hands_won',
'games_played',
'high_score',
'low_score',
]
Statistics Model:
class Statistics(models.Model):
# Define model fields:
user = models.OneToOneField(User, primary_key=True)
games_won = models.IntegerField(null=True, blank=True)
hands_won = models.IntegerField(null=True, blank=True)
games_played = models.IntegerField(null=True, blank=True)
high_score = models.IntegerField(null=True, blank=True)
low_score = models.IntegerField(null=True, blank=True)
def __str__(self):
return str(self.pk)
def increment_games_won(self, is_winner):
if is_winner is True:
self.games_won = self.games_won + 1
return self.games_won
def add_to_hands_won(self, num_hands_won):
if num_hands_won > 0 and num_hands_won < 8:
self.hands_won = self.hands_won + num_hands_won
return self.hands_won
def increment_games_played(self):
self.games_played = self.games_played + 1
return self.games_played
def new_high_score(self, score):
if score > self.high_score:
self.high_score = score
return self.high_score
def new_low_score(self, score):
if score < self.low_score:
self.low_score = score
return self.low_score
Statistics ViewSet:
class StatisticsViewSet(DefaultsMixin, viewsets.ModelViewSet):
queryset = Statistics.objects.all()
serializer_class = StatisticsSerializer
filter_class = StatisticsFilter
search_fields = ('pk', 'user')
ordering_fields = ('games_won', 'hands_won', 'games_played', 'high_score', 'low_score')
def update(self, request, pk=None):
stats = self.get_object()
stats.increment_games_won(request.data['is_winner'])
stats.add_to_hands_won(request.data['num_hands_won'])
stats.increment_games_played()
stats.new_low_score(request.data['score'])
stats.new_high_score(request.data['score'])
stats.save()
serialized_stats = StatisticsSerializer(stats, context={'request': request}).data
return Response(serialized_stats)
You could probably use another Serializer and use it for you PUT API
StatisticsUpdateSerializer:
class StatisticsUpdateSerializer:
is_winner = ...
num_hands_won = ...
score = ...
And use this serializer in the PUT API or create a new route as shown in the example mentioned in the DRF documentation here
#detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
// Use your serializer below
serializer = PasswordSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.data['password'])
user.save()
return Response({'status': 'password set'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
I got this model:
class Like(models.Model):
id = models.AutoField(primary_key=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
post = models.ForeignKey('posts.Post', blank=True, null=True)
RATING_CONVERSION = (
(1, '+'),
(0, '0'),
(-1, '-'),
)
userRating = models.SmallIntegerField(choices=RATING_CONVERSION)
def __int__(self):
return self.id
If post id and user id exists I need to update rating.
I try to make it.
Serializer:
class LikeSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(queryset=ExtUser.objects.all(), required=False, allow_null=True,default=None)
class Meta:
model = Like
field = ('user', 'post')
validators = [
UniqueTogetherValidator(
queryset=Like.objects.all(),
fields=('user', 'post')
)
]
def validate_user(self, value):
return self.context['request'].user
def create(self, validated_data):
return Like.objects.create(**validated_data)
And ViewSet
class LikeViewSet(viewsets.ModelViewSet):
queryset = Like.objects.all()
serializer_class = LikeSerializer
#detail_route(methods=['post', 'get'])
def get_object(self):
if self.request.method == 'POST':
like = Like.objects.get(user=self.context['request'].user, post = self.context['request'].post)
if like:
return like
else:
return Like(id=self.kwargs.get('pk'))
else:
return super(LikeViewSet, self).get_object()
I found a lot of information how to create or update using pk in Url, but I got parameters inside of JSON
This method doesn't work - instead of update rating it creates new Model objects