How to create a ModelViewset when model has OneToOneField? - django

The idea is to have a model like this:
class Object(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=256)
def __str__(self):
return self.name
and then create a serializer and a ModelViewSet related to it.
The problem is that ModelViewSet must return a queryset but I want to work with single objects due to the one-to-one relation.
How should I proceed?

As mentioned in the documentation:
The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
This means that ModelViewSet also takes care of situation where a specific Object should be returned. You can even override def get_object(self) if you want a behavior more complex than what is implemented (see this SO question).
However, to really ensure that only one Object is returned, I would advice you to use a field as a unique primary key. You would populate this field on insertion (manually or automatically) and then use it as a lookup_field.
Below, I assumed that you already had a __str__(self) function in your User model as well.
Then, your Object model would become:
class Object(models.Model):
id = models.BigAutoField(primary_key=True) # or another unique primary_key field
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=256)
def __str__(self):
return f"{self.name} ({self.id})"
Your serializer would be:
class ObjectSerializer(serializers.ModelSerializer):
class Meta:
model = Object
fields = ("id", "name", "user")
id = serializers.CharField(required=True)
Your ModelViewSet would be something like:
class ObjectViewSet(viewsets.ModelViewSet):
http_method_names = ["get"]
# parser_classes = [JSONParser] # Replace with your parser class
queryset = Object.objects.all()
serializer_class = ObjectSerializer
lookup_field = "id"
Finally, to make the whole thing works you would also have to register your ModelViewSet in your urlpatterns.
I use to explicitly bind my ViewSet classes into a set of concrete views but this is more of a personal choice so it's up to you. You can check the corresponding documentation for more info.
urlpatterns = [
...
path(
r"objects/<str:id>",
ObjectViewSet.as_view({"get": "retrieve"}),
),
...
]
With all this, a request to <BASE_URL>/objects/<YOUR_OBJECT_ID> should return only one Object result.

Related

DRF tries to create User object when used in nested serializer

DRF docs says that "By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be save."<<
In general this is true except for one case. When User class is a nested object in serializer.
Use case is to add existing user to newly created organization.
Consider my simple example where I noticed that issue:
views.py
class UserOrganizationViewSet(viewsets.ModelViewSet):
# authentication_classes = (JwtAuthentication, SessionAuthentication)
# permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,)
serializer_class = UserOrganizationSerializer
queryset = UserOrganization.objects.all()
models.py:
class UserOrganization(models.Model):
name = models.CharField(max_length=256)
users = models.ManyToManyField(User, blank=True, related_name="user_organizations", through='OrganizationMember')
def __str__(self):
return self.name
class OrganizationMember(models.Model):
organization = models.ForeignKey(UserOrganization, on_delete=CASCADE)
user = models.ForeignKey(User, on_delete=CASCADE)
joined = AutoCreatedField(_('joined'))
serializers.py:
First version - User class has its own simple serializer and it is used as nested serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ['username']
class UserOrganizationSerializer(serializers.ModelSerializer):
users = UserSerializer(many=True, required=False)
class Meta:
model = UserOrganization
fields = ['id', 'name', 'users']
read_only_fields = ['id']
Is this case I am able to fetch data via GET method (Organization created through admin panel):
But when I am trying to create organization I am getting error which says that user already exist:
So, I checked what will happen when I try to paste not existed username in JSON data send. According to expectation DRF says that I need to override create() method:
{
"name": "Test2",
"users": [
{
"username": "admins"
}
]
}
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `organization.serializers.UserOrganizationSerializer`, or set `read_only=True` on nested serializer fields.
And there is a first weird behavior. DRF still tries to created User object:
My create method looks like this:
def create(self, validated_data):
print("validated_data", validated_data)
users = validated_data.pop('users', [])
user_organization = UserOrganization.objects.create(**validated_data)
print("users", users)
for user in users:
userSerializer = UserSerializer(user)
print("userSerializer.data", userSerializer.data)
username = userSerializer.data['username']
print("username", username)
user_organization.users.add(get_user_model().objects.get(username=username))
return user_organization
In this case my method isn't called at all.
In case when I passed user which does not exist, my create() method is called and exeption is thrown as it should be. (I know I am not handling errors, this is just sandbox):
{
"name": "Test2",
"users": [
{
"username": "admin1"
}
]
}
django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.
New model class UserProfile and serializer for this class used as nested:
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = ['name']
class UserOrganizationSerializer(serializers.ModelSerializer):
profiles = UserProfileSerializer(many=True)
class Meta:
model = UserOrganization
fields = ['id', 'name', 'profiles']
read_only_fields = ['id']
def create(self, validated_data):
print("validated_data ", validated_data)
users = validated_data.pop('profiles', [])
user_organization = UserOrganization.objects.create(**validated_data)
print("create", users)
for user in users:
print("username = ", user['name'])
user_organization.profiles.add(UserProfile.objects.get(name=user['name']))
user_organization.save()
return user_organization
Small change was needed to change field users to profiles inside UserOrganization.
Creating organization with existed user profile (created by admin) via POST:
In this scenario there is no such problem as mentioned in first case.
Final solution using SlugReleatedField works as expected. No serializer used for User.
class UserOrganizationSerializer(serializers.ModelSerializer):
users = serializers.SlugRelatedField(many=True, queryset=get_user_model().objects.all(), slug_field='username')
class Meta:
model = UserOrganization
fields = ['id', 'name', 'users']
read_only_fields = ['id']
Above code does what I exactly need and is very simple. Here is small concern regarding performance when fetched all users from DB in this line:
queryset=get_user_model().objects.all()
Can someone explain why this happens what we could observe in scenario 1?
Sorry for this post being so long. I tried to shorten it as much I as can. I can put whole project in github if it will be needed for readability.

How to use serializer method field to work on primary key related data

I want to use serializer method field to fetch data with some business rule
class Product(models.Model):
name = models.CharField()
class Stock(models.Model):
product = models.ForeignKey(Product, on_delete=models.Cascade, related_name='stock')
current = models.BooleanField(default=True)
quantity = models.IntegerField()
class ProductSerializer(serializers.ModelSerializer):
productstock = serializers.SerializerMethodField()
#GET LATEST CURRENT STOCK-QUANTITU
class Meta:
model = Product
fields = [
'name',
'productstock'
]
I want to get an output like this:
{
name:'laptop',
productstock:18
}
I was not able check it right now but it should be something like this:
class ProductSerializer(serializers.ModelSerializer):
productstock = serializers.SerializerMethodField()
#GET LATEST CURRENT STOCK-QUANTITU
class Meta:
model = Product
fields = [
'name',
'productstock'
]
def get_productstock(self, obj):
stock = Stock.objects.filter(product__name=obj.name)
return stock.quantity
Explanation:
the serializer method is always built up by get_ + variable_name and takes the curren object as an argument. Then you should be able to filter your Stock model and the productstock value will become what you return by this method. Hope it helps you.
Update
I guess now I got your point. You should do the filtering before you pass your queryset through the serializer. Therefore I would recommend to override the get_queryset method. In your view just add the below function and it just filters only the products with current=True. My example is with APIView but it works with every DRF View as they all inherit the APIView.
class YourViewClass(APIView):
...
def get_queryset(self):
return Product.objects.filter(stock__current=True)
Explanation
Within the filter method you refer to your related_name of the foreign key field and then use double underscore plus the field you want to filter on. Hope it works for you.

Identify Django Rest Framework ModelSerializer ForeignKey with attribute other than pk

I feel like this is a super basic question but am having trouble finding the answer in the DRF docs.
Let's say I have a models.py set up like so:
#models.py
class Person(models.Model):
name = models.CharField(max_length=20)
address = models.CharField(max_length=20)
class House(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Person)
And I have a ModelSerializer set up like so:
#serializers.py
class House(serializers.ModelSerializer):
class Meta:
model = House
fields = '__all__'
What I want to do is to be able to POST new House objects but instead of having to supply the pk of the Person object, I want to be able to supply the name of the Person object.
E.g.
post = {'name': 'Blue House', 'owner': 'Timothy'}
The actual models I'm using have several ForeignKey fields so I want to know the most canonical way of doing this.
One solution may be to use a SlugRelatedField
#serializers.py
class House(serializers.ModelSerializer):
owner = serializers.SlugRelatedField(
slug_field="name", queryset=Person.objects.all(),
)
class Meta:
model = House
fields = '__all__'
This will also change the representation of your serializer though, so it will display the Person's name when you render it. If you need to render the Person's primary key then you could either override the House serializers to_representation() method, or you could implement a small custom serializer field by inheriting SlugRelatedField and overriding to_representation() on that instead.
Change your serializer as below by overriding the create() method
class House(serializers.ModelSerializer):
owner = serializers.CharField()
class Meta:
model = House
fields = '__all__'
def create(self, validated_data):
owner = validated_data['owner']
person_instance = Person.objects.get(owner=owner)
return House.objects.create(owner=person_instance, **validated_data)

Filtering a reverse relationship by user, and returning only one object in Django Rest Framework

Given these models:
class Product(models.Model):
name = CharField(max_length=255)
class Order(models.Model):
product = models.ForeignKey(Product)
user = models.ForeignKey(User)
quantity = models.PositiveIntegerField()
A user can only have a single Order object per product. I would like an API call to show a list of products, with the user's order where available. Is there a way to do that?
The default serialisation lists ALL orders under order_set. I did get a bit ahead with this to filter by user:
class FilteredOrderSerializer(serialisers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.context['request'].user)
return super(FilteredOrderSerializer, self).to_representation(data)
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
list_serializer_class = FilteredOrderSerializer
class ProductSerializer(serializers.ModelSerializer):
order_set = OrderSerializer(many=True, read_only=True)
class Meta:
model = Product
So now it only shows the authenticated user's order, but the resulting JSON looks something like
[
{
"name": "prod1",
"order_set": [
{
"quantity": 4
}
],
}
]
i.e. the field is still called order_set and it's a list, while I would like it to be called order and be either an object or null. So I'm not sure where this filtering should take place, nor how to define the field.
Edit: I'm using a simple viewset
class ProductViewSet(view sets.ReadOnlyModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
You need to add related_name field in your orders so you can
product.orders.all()
Then this should do.
class Order(models.Model):
product = models.ForeignKey(Product, related_name='orders')
user = models.ForeignKey(User)
quantity = models.PositiveIntegerField()
class ProductSerializer(serializers.ModelSerializer):
order = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ('name', 'order')
def get_order(self, object):
try:
order = object.orders.get(user=self.context['request'].user)
return SceneDetailSerializer(order).data
except Order.DoesNotExist:
return None
Update: You can try out serializer method field. Not sure if self contains context and user references in it. You need to remove listfield from your order serializer. Let me know if it works.

django admin for abstract base class?

Suppose I have car - suv/bus models.
I'd like to list all cars in the django admin (with name attribute).
When user clicks one of the car, it would go to the appropriate detail page of either suv or bus model.
How do I create a such admin list page?
class Car:
name = models.CharField()
class Meta:
abstract=True
class Suv(Car):
pass
class Bus(Car):
pass
Not sure this is the best approach, but sharing my solution here.
First, create a Database View
create view [app_label_model_name] as
select id, name, 1 as car_type
from suv
union
select id, name, 2 as car_type
from bus
order by something;
then create non-manged model
class PostBaseView(models.Model):
# this would be CarBaseView, I'm copying my actual code
id = models.IntegerField()
raw_html = models.TextField(null=True)
created_at = models.DateTimeField(primary_key=True)
post_type = models.IntegerField()
class Meta:
managed = False
then, from admin page, change the links based on subclass types.
class ChangeList(ChangeListDefault):
def url_for_result(self, result):
# pk = getattr(result, self.pk_attname)
id = result.id
app_label = result.get_app_label()
model_name = result.get_model_name()
return reverse('admin:%s_%s_change' % (app_label,
model_name),
args=(quote(id),))
class PostBaseViewAdmin(admin.ModelAdmin):
list_display = ['__str__', 'post_type_str']
class Meta:
model = PostBaseView
def get_changelist(self, request, **kwargs):
"""
Returns the ChangeList class for use on the changelist page.
"""
return ChangeList
admin.site.register(PostBaseView, PostBaseViewAdmin)
volla you have a admin that shows multiple subclasses at one list.
eugene's answer took me to the right direction. Thank you.
I'm not aware if there is a better way either, but in my case this approach solved the problem.
I already had a base class (Project) with some generic fields (SportProject and others), and specific classes that extends Project with their fields, and I wanted to have a single list of project, but wanted to have specific edit form for each project type.
In app/admin.py file, I did:
from django.contrib.admin.views.main import ChangeList as ChangeListDefault
from django.urls import reverse
class ProjectChangeList(ChangeListDefault):
def url_for_result(self, result):
app_label = result._meta.app_label.lower()
model_name = result.children._meta.object_name.lower()
return reverse('admin:%s_%s_change' % (app_label, model_name), args=(result.id,))
class ProjectDataAdmin(admin.ModelAdmin):
#...
def get_changelist(self, request, **kwargs):
"""
Returns the ChangeList class for use on the changelist page.
"""
return ProjectChangeList
In my model Project, I have a children property that returns the children instance (SportProject for instance). Not sure if there is another way to have this structure in Django.
I also had all classes registered in django-admin (Project and all children, as SportProject), to have django-admin pages for those classes. Thus, I'm using django-modeladmin-reorder to hide undesired links on django-admin main page.
Hope it helps someone.
You need to make your Car not an abstract model, as you have to have some base table for your vehicles.
class Car:
name = models.CharField()
class Meta:
abstract = False
class Suv(Car):
pass
class Bus(Car):
pass
class CarAdmin(admin.ModelAdmin):
model = Car
But here the easy things end. The admin doesn't have a built-in solution for your task. I suggest you overriding at least the CarAdmin.get_form() and CarAdmin.get_queryset() methods, though I'm not sure they are the only ones you have to customize.
I think this answer is applicable. ModelAdmin needs to know foreign key entries will be readonly, specified in a tuple called readonly_fields.
Using the problem that brought me here and there, I have (models.py):
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
def __str__(self):
return self.choice_text
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, default = 1)
class Meta:
abstract = True
class Vote(Answer):
choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
def answer(self):
return self.choice
def __str__(self):
return self.choice.choice_text
And (admin.py):
class VoteAdmin(admin.ModelAdmin):
#list_display = ('Answer.question.question_text', 'Answer.User.user_id', 'Choice.choice_text')
readony_fields = ('question', 'user')
list_display = ('question', 'user', 'choice')
fieldsets = [
('Question', {'fields': ['question']}),
('User', {'fields': ['user']}),
('Vote', {'fields' : ['choice']}),
]
Hope this proves useful to future searchers.