Django Rest Framework custom readonly field dependant on related model - python

supposing a models.py like:
class User(AbstractUser):
pass
class Notification(models.Model):
value = models.CharField(max_length=255)
class NotificationUserLink(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
notification = models.ForeignKey(Notification, on_delete=models.CASCADE)
active = models.BooleanField(default=True)
The point behind normalizing instead of just having a user field on the Notification itself is for when I want to send the same Notification to hundreds of people I only need 1 Notification object. Suppose I also have an endpoint in views.py like below where the user can see all their notifications and only their notifications, which is achieved by filtering the queryset.
class NotificationViewSet(viewsets.ModelViewSet):
serializer_class = serializers.NotificationSerializer
model = Notification
lookup_field='pk'
def get_queryset(self):
user = self.request.user
return Notification.objects.filter(notificationuserlink__user=user)
Now the "user" and "notification" pair on the NotificationUserLink model form a candidate key (they are unique together. currently just by business logic but I'll add the actual constraint to the db at some point).
Therefore, given a user in the request, each Notification (in the filtered queryset) will have 1 and only 1 NotificationUserLink to that User. Therefore, from the User's perspective, that notification is either active or it isn't, depending on the "active" field on the NotificationUserLink.
What I want to accomplish is to include that field ("active") in the serialization of the Notification. I.e. if the user hits this endpoint they should see notifications like:
[
{
"value": "Bob commented on your post",
"active": False
},
{...}, ...
]
etc
I think that to achieve this I will need to override the "list" method on the view. But I don't know how. The serializer is being passed a queryset in the list method so I'm not sure how to inject data into that.
The serializer currently just looks like so:
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model=Notification
fields=("value",)

You can annotate the status of the link like this in your queryset:
Notification.objects.filter(
notificationuserlink__user=user
).annotate(
active=F('notificationuserlink__active')
)
And then you can add a read-only field active field in your serializer:
class NotificationSerializer(serializers.ModelSerializer):
active = serializers.BooleanField(read_only=True)
class Meta:
model = Notification
fields = ("active", "value",)

Related

How to POST an entity specifiying an integer foreign key in django rest framework

I am building a chat application with django rest framework and I m currently working on messages. This are my models:
from django.db import models
from django.contrib.auth.models import User
class Message(models.Model):
text = models.CharField(max_length=500)
datetime = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE);
I am using the Django auth User model. This is my ModelViewSet for the messages:
class MessageViewSet(ModelViewSet):
queryset = Message.objects.all()
serializer_class = MessageSerializer
And these are my serializers:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username']
class MessageSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = Message
fields = '__all__'
And this is my API:
The code I've written so far works really well for the GET functionally I want. I want for each message to get the username of the user it belongs to. But now I want the following thing: when I POST a new message, I want to be able to specify which user it belongs to by specifying the user's id. Right now I have only the "text" field in the POST section. I need to add a "user" field which takes in an integer (the user primary key) to specify which user the message belongs to. How should I refactor my code in order to do that?
Because you've overridden the user field and set it to read_only=True, you cannot set a user when you're creating/updating a model.
If you just need the user's username, I'd suggest you to add a username field into MessageSerializer directly instead:
class MessageSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Message
fields = '__all__'
Now you'll get this payload instead:
{
"id": 1,
"user": 1,
"username": "timi",
...
And you should be able to set a user id now.

How to add the current user to a many-to-many field when creating via a viewset?

I have a Django project which has "workspaces", and users can belong to multiple workspaces, so there's a many-to-many relationship between the users and the workspaces.
Now, everything else works fine so far, but I'm having trouble adding the current user to the workspace's list of users when the user creates that workspace.
The model looks like this:
class Workspace(models.Model):
name = models.CharField(max_length=100)
users = models.ManyToManyField(User, related_name='workspaces')
The serializer looks like this:
class WorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = Workspace
fields = ('name', 'users')
And finally, the view:
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all().order_by('name')
serializer_class = WorkspaceSerializer
permission_classes = [permissions.IsAuthenticated,]
So, to elaborate, when a user creates a new workspace, I'd like the workspace's users field to refer to that user. Currently, that field remains empty. How would I populate the field so that the current user is added there right on creation?
You can override the create method of the serializer class:
class WorkspaceSerializer(serializers.ModelSerializer):
# ...
def create(self, validated_data):
instance = super(WorkspaceSerializer, self).create(validated_data)
instance.users.add(self.context['request'].user)
return instance
Note that you have access to the request in the serializer via the context attribute. This is passed in by the corresponding view (source).

DRF Serializer field for intermediate mode

I am making am app to control the presence of students. I have 4 models:
class Student(models.Model):
name = models.CharField(max_length=70)
class Justification(models.Model):
name = models.CharField(max_length=70)
class Session(models.Model):
date = models.DateTimeField()
present = models.ManyToManyField(Student)
absences = models.ManyToManyField(Student, related_name='absences_set', through='Absence')
class Absence(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE)
atleta = models.ForeignKey(Student, on_delete=models.CASCADE)
justification = models.ForeignKey(Justification, on_delete=models.CASCADE)
The models have more fields and different names (I translated the names to English) but this is basically it.
I am using DRF framework to make an API. I have setup the endpoints (and serializers) for Student, Justification and Absence but I can't figure out how to make the serializer for the Session model. I want it to work when someone makes the following POST (I only need an endpoint to create Sessions) request (I am using a ViewSet for the view):
{
"date": "2019-02-01T10:08:52-02:00"
"present": [
2
],
"absences": [
{
"student": 1,
"justification": 1
}
]
}
But the absences are not created. How can I make this nested relationship work?
ps: I can only make one request that's why I don't want to make one request to create a Session and then many requests to create Absences and need it all together. If there is a way to create all of them on the same request (but not only the same JSON object) I am okay with this solution
If i understand properly you want to create corresponding absences and season at same Season end-point. I think Justification and Student both model serve same, they are just student's instance and keep student information if i am not wrong. So i don't think there is actually any need to keep Justfication model. Corresponding absences ( students ) in Season Model need to ask for Justification. So my advice to keep model structure as like these
class Student(models.Model):
name = models.CharField(max_length=70)
class Session(models.Model):
date = models.DateTimeField()
present = models.ManyToManyField(Student)
absences = models.ManyToManyField(Student, related_name='absences_set', through='Absence')
class Absence(models.Model):
session = models.OneToOneField(Session, on_delete=models.CASCADE) # You can also keep Foreign-key
atleta = models.ForeignKey(Student, on_delete=models.CASCADE)
And then there are two possible way to create Absence model instance corresponding to Season Post endpoint. We can overwrite the post method of SeasonViewset and write our logic there or even can overwrite the SeasonSrealizer-create method to do same.
My preferable option is to overwrite post method of SeasonViewset. And these can be done as like following - over writing DRF CreateMixins
class SeasonViewSet(viewsets.ModelViewSet):
# your declare serializers and others thing
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
season_instance = self.perform_create(serializer)
# creating Absence's instance and you need to add other fields as necessary
Absence.objects.create(season=season_instance)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

Display a different default value in Django CreateView according to current logged in user?

Long story short, I am creating a ModelForm which will be passed to a generic CreateView. This form will allow a user to post an event with a location. I have already collected the user's base address in my user creation form, and my Event model has a foreignkey to the author of the event.
I would like to display the city and state of the currently logged in user as a default value on the event creation form. Since default values are set at the model level, I am not able to use the requests framework. Solution such as this one offer a way to save info from the request to the database upon submission but I would like to display this default when the user first navigates to the form page. How do I achieve this functionality?
Edit I would like to be able to pass an initial parameter as in this post but have it dynamically determined by the current logged in user.
Here is the essential part of the model. Note that BusinessUser has city and state fields.
class Job(models.Model):
author = models.ForeignKey(BusinessUser, on_delete = models.CASCADE)
location = models.CharField(max_length=200, blank=True, help_text="If different from the location listed on your company profile.")
And here is the view so far:
class JobCreation(LoginRequiredMixin, SuccessMessageMixin, generic.edit.CreateView):
model = Job
form_class = JobCreationForm
context_object_name = 'job'
success_url = reverse_lazy('jobs:business_profile')
success_message = 'New job posted!'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
JobCreationForm is the work in progress that I'm stuck on. At the moment, it's a ModelForm giving the model and fields.
class JobCreationForm(ModelForm):
class Meta:
model = Job
fields = (
'job_title',
'location',
)
You can override get_initial if you want to set initial data dynamically. If you don't need to set it dynamically you can simply set initial.
class JobCreation(LoginRequiredMixin, SuccessMessageMixin, generic.edit.CreateView):
model = Job
form_class = JobCreationForm
def get_initial(self):
initial = super().get_initial()
initial['location'] = self.request.user.location
return initial
...

How does Django Rest Framework deserialize foreign key relationships?

In the tutorial, there's this loose one-to-one mapping between serializer fields and model fields. I can expect that if a serializer field and a model field are both CharFields it will save a string of characters when deserializing into a model instance:
models.py:
class Deck(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, unique=True, blank=False, null=False)
serializers.py:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Deck
fields = ('url', 'id', 'created', 'name')
extra_kwargs = {
'url': {
'view_name': 'goals:deck-detail',
}
}
But when I try a relationship, the serializer field is a ReadOnlyField, which from what I understand is essentially a Charfield, but the model field is a ForeignKeyField, and to add to the confusion, it seems like I'm saving an object in the views when I override perform_create:
models.py:
class Deck(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, unique=True, blank=False, null=False)
user = models.ForeignKey('users.User', related_name='decks', on_delete=models.CASCADE, null=False)
serializers.py:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
class Meta:
model = Deck
fields = ('url', 'id', 'created', 'name', 'user')
extra_kwargs = {
'url': {
'view_name': 'goals:deck-detail',
}
}
views.py:
class DeckList(generics.ListCreateAPIView):
serializer_class = DeckSerializer
def get_queryset(self):
return Deck.objects.all().filter(user__username=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
What's going on here? When dealing with relationships, why is it that I am saving an object by overriding perform_create in the views, declaring in the serializers a CharField, and in the models declaring a relationship field?
What's missing in my understanding, or what's really happening under the hood such that the user field (ForeignKey) can be represented as a string but saved as an object?
Edit:
If I'm overriding serializer.save(user=user) in the views and the serializers.py has the user field as
user = serializers.CharField(read_only=True)
And I want to override the save method in serializers.py, how do I pass the proper data so that it will know how to serialize? Do I just grab the whole User object, save it, and it'll do the rest? Is the serializers.save() override in the views the same as serializers.save() in serializers.py?
Not 100% sure that I've understood what you're asking, but if the question is:
What's happening under the hood when a ForeignKey field is saved in the Django ORM?
Then the answer is that:
The relation is saved in the DB as an (e.g.) int field which stores the primary key of the related object.
The ForeignKey field reference section in the Django docs explains how this part of the ORM works, and the "Database Representation" subsection likely touches on the specific bit you're interested in.
For example, for your case of User being a related field in the Deck model the underlying table would likely look like this (assuming postgresql):
myapp_deck
id int
created timestamp
name varChar
user_id int
The Deck -> User relation is mapped by the DB storing the pk for the related User object in the user_id field in the myapp_deck table.
So, all Django (and, consequently, DRF) needs to do to change the User in the Deck model is change the user_id in the myapp_deck table to the PK of a different User object.
Hope this helps, and please let me know if I've missed the point of your question.
Edited to Add Example of Custom .create() method
If you want to override the custom "save" method in a serializer then the methods to override are create() and update() accordingly (see Serializer "Saving instances" section in the DRF docs).
An example of this might be:
class DeckSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
... Rest of your serializer code ...
def create(self, validated_data, **kwargs):
user_obj = User.objects.get(pk=validated_data["user"])
deck = Deck.objects.create(
name=validated_data["name"],
user=user_obj,
)
return deck
Note: This assumes that the pk of the related User object is passed throught the serializer, validates OK, and is available in the validated_data dict.

Categories