I'm trying to create an API endpoint for my django project. In this project, i have two databases: a SQLite database and a MongoDB database; the data that i'm trying to retrieve is on my Mongo database, on a collection called tst.
So on this collection there is already some data. I created the endpoint, then i opened my browser to go to http://127.0.0.1:8000/tst/, expecting to find that data there in json format, but it looks like the endpoint doesn't see any data, although i'm sure it is.
Can someone help me find what i'm doing wrong?
Here is my model:
class tst(models.Model):
id = models.CharField(max_length=100)
ticker = models.FloatField()
def save(self): # ALL the signature
super(tst, self).save(using='dbtwo')
Here is my view:
class tstList(generics.ListCreateAPIView):
queryset = tst.objects.using('dbtwo').all()
serializer_class = tstSerializer
Here is the serializer:
class tstSerializer(serializers.ModelSerializer):
class Meta:
model = tst
fields = ('id', 'ticker', )
And the url:
path('tst/', views.tstList.as_view()),
I got these two errors on my console:
TypeError: Got a `TypeError` when calling `tst.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `tst.objects.create()`. You may need to make the field read-only, or override the tstSerializer.create() method to handle this correctly.
And
TypeError: save() got an unexpected keyword argument 'force_insert'
It seems you're only concerned about the using keyword argument in the overriden save method; you can use wildcard arguments for others e.g.:
class tst(models.Model):
id = models.CharField(max_length=100)
ticker = models.FloatField()
def save(self, *args, using=None, **kwargs):
super(tst, self).save(*args, using='dbtwo', **kwargs)
This does not want you to put all other irrelevant arguments in the method signature.
As you're overriding the save method, you need to pass all its signature in order to work. Also, you have to override your create method in your serializer.
class tst(models.Model):
id = models.CharField(max_length=100)
ticker = models.FloatField()
def save(self, force_insert=False, force_update=False,
using='dbtwo', update_fields=None):
super(tst, self).save(force_insert=force_insert,
force_update=force_update,
using=using, update_fields=update_fields)
class tstSerializer(serializers.ModelSerializer):
class Meta:
model = tst
fields = ('id', 'ticker', )
def create(self, validated_data):
return tst.objects.create(**validated_data)
Related
SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.
I have two Django models:
from django.db import models
class Policy(models.Model):
status = models.IntegerField()
def save(self, *args, **kwargs):
quote = self.documents.get(document_type=DocumentType.quote)
if self.status == 0:
quote.delete()
elif self.status == 1:
new_quote_content = create_new_quote()
quote.s3_file.save(quote.name, File(new_quote_content))
super().save(*args, *kwargs)
class Document(models.Model):
policy = models.ForeignKey(
to=Policy,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="documents",
)
s3_file = models.FileField(
storage=S3Storage(aws_s3_bucket_name="policy-documents"),
upload_to=get_document_s3_key,
max_length=255,
)
I want to delete/update the document when the policy status is updated and I've overriden the save() method in Policy to do it. However, neither the doc deletion nor the doc's FieldFile update works in the save() method. If I move them to outside the save() method, everything works.
Does someone understand what's the issue here?
It is not calling the super method to save the model. To override a model it has to be something like this as given in the documentation of Django.
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, *args, **kwargs):
do_something()
super().save(*args, **kwargs) # Call the "real" save() method.
do_something_else()
The save() was being called from a Policy ModelAdmin with a inlined Document form set. After it was run, Django executed the ModelAdmin's save_related() method, which saved the Document form set data, overwriting the Document changes I had just saved in save(). I solved it by overriding save_related() and deleting/updating the document after form.save_m2m() and form.save_formset().
With Django REST Framework, a standard ModelSerializer will allow ForeignKey model relationships to be assigned or changed by POSTing an ID as an Integer.
What's the simplest way to get this behavior out of a nested serializer?
Note, I am only talking about assigning existing database objects, not nested creation.
I have hacked away around this in the past with additional 'id' fields in the serializer and with custom create and update methods, but this is such a seemingly simple and frequent issue for me that I'm curious to know the best way.
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# phone_number relation is automatic and will accept ID integers
children = ChildSerializer() # this one will not
class Meta:
model = Parent
Updated on July 05 2020
This post is getting more attention and it indicates more people have a similar situation. So I decided to add a generic way to handle this problem. This generic way is best suitable for you if you have more serializers that need to change to this format
Since DRF doesn't provide this functionality out of the box, we need to create a serializer field first.
from rest_framework import serializers
class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.serializer else True
def to_representation(self, instance):
if self.serializer:
return self.serializer(instance, context=self.context).data
return super().to_representation(instance)
I am not well impressed with this class name, RelatedFieldAlternative, you can use anything you want.
Then use this new serializer field in your parent serializer as,
class ParentSerializer(ModelSerializer):
child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)
class Meta:
model = Parent
fields = '__all__'
Original Post
Using two different fields would be ok (as #Kevin Brown and #joslarson mentioned), but I think it's not perfect (to me). Because getting data from one key (child) and sending data to another key (child_id) might be a little bit ambiguous for front-end developers. (no offense at all)
So, what I suggest here is, override the to_representation() method of ParentSerializer will do the job.
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Complete representation of Serializer
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
fields = '__all__'
class ParentSerializer(ModelSerializer):
class Meta:
model = Parent
fields = '__all__'
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Advantage of this method?
By using this method, we don't need two separate fields for creation and reading. Here both creation and reading can be done by using child key.
Sample payload to create parent instance
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}
Screenshot
The best solution here is to use two different fields: one for reading and the other for writing. Without doing some heavy lifting, it is difficult to get what you are looking for in a single field.
The read-only field would be your nested serializer (ChildSerializer in this case) and it will allow you to get the same nested representation that you are expecting. Most people define this as just child, because they already have their front-end written by this point and changing it would cause problems.
The write-only field would be a PrimaryKeyRelatedField, which is what you would typically use for assigning objects based on their primary key. This does not have to be write-only, especially if you are trying to go for symmetry between what is received and what is sent, but it sounds like that might suit you best. This field should have a source set to the foreign key field (child in this example) so it assigns it properly on creation and updating.
This has been brought up on the discussion group a few times, and I think this is still the best solution. Thanks to Sven Maurer for pointing it out.
Here's an example of what Kevin's answer is talking about, if you want to take that approach and use 2 separate fields.
In your models.py...
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
then serializers.py...
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# if child is required
child = ChildSerializer(read_only=True)
# if child is a required field and you want write to child properties through parent
# child = ChildSerializer(required=False)
# otherwise the following should work (untested)
# child = ChildSerializer()
child_id = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all(), source='child', write_only=True)
class Meta:
model = Parent
Setting source=child lets child_id act as child would by default had it not be overridden (our desired behavior). write_only=True makes child_id available to write to, but keeps it from showing up in the response since the id already shows up in the ChildSerializer.
There is a way to substitute a field on create/update operation:
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
child = ChildSerializer()
# called on create/update operations
def to_internal_value(self, data):
self.fields['child'] = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all())
return super(ParentSerializer, self).to_internal_value(data)
class Meta:
model = Parent
A few people here have placed a way to keep one field but still be able to get the details when retrieving the object and create it with only the ID. I made a little more generic implementation if people are interested:
First off the tests:
from rest_framework.relations import PrimaryKeyRelatedField
from django.test import TestCase
from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer
from .factories import SomethingElseFactory
from .models import SomethingElse
class TestModelRepresentationPrimaryKeyRelatedField(TestCase):
def setUp(self):
self.serializer = ModelRepresentationPrimaryKeyRelatedField(
model_serializer_class=SomethingElseSerializer,
queryset=SomethingElse.objects.all(),
)
def test_inherits_from_primary_key_related_field(self):
assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField)
def test_use_pk_only_optimization_returns_false(self):
self.assertFalse(self.serializer.use_pk_only_optimization())
def test_to_representation_returns_serialized_object(self):
obj = SomethingElseFactory()
ret = self.serializer.to_representation(obj)
self.assertEqual(ret, SomethingElseSerializer(instance=obj).data)
Then the class itself:
from rest_framework.relations import PrimaryKeyRelatedField
class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.model_serializer_class = kwargs.pop('model_serializer_class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False
def to_representation(self, value):
return self.model_serializer_class(instance=value).data
The usage is like so, if you have a serializer somewhere:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
This will allow you to create an object with a foreign key still only with the PK, but will return the full serialized nested model when retrieving the object you created (or whenever really).
There is a package for that! Check out PresentablePrimaryKeyRelatedField in Drf Extra Fields package.
https://github.com/Hipo/drf-extra-fields
I think the approach outlined by Kevin probably would be the best solution, but I couldn't ever get it to work. DRF kept throwing errors when I had both a nested serializer and a primary key field set. Removing one or the other would function, but obviously didn't give me the result I needed. The best I could come up with is creating two different serializers for reading and writing, Like so...
serializers.py:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = Child
class ParentSerializer(serializers.ModelSerializer):
class Meta:
abstract = True
model = Parent
fields = ('id', 'child', 'foo', 'bar', 'etc')
class ParentReadSerializer(ParentSerializer):
child = ChildSerializer()
views.py
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = Parent.objects.all()
def get_serializer_class(self):
if self.request.method == 'GET':
return ParentReadSerializer
else:
return self.serializer_class
Here's how I've solved this problem.
serializers.py
class ChildSerializer(ModelSerializer):
def to_internal_value(self, data):
if data.get('id'):
return get_object_or_404(Child.objects.all(), pk=data.get('id'))
return super(ChildSerializer, self).to_internal_value(data)
You'll just pass your nested child serializer just as you get it from the serializer ie child as a json/dictionary. in to_internal_value we instantiate the child object if it has a valid ID so that DRF can further work with the object.
I started by implementing something similar to JPG's solution before I found this answer, and noticed that it breaks the built-in Django Rest Framework's templates. Now, that isn't such a big deal (as their solution works wonderfully via requests/postman/AJAX/curl/etc.), but if someone's new (like me) and wants the built-in DRF form to help them along the way, here's my solution (after cleaning it up and integrating some of JPG's ideas):
class NestedKeyField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('You need to pass a instance of serialzers.Serializer or atleast something that inherits from it.')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return not self.serializer
def to_representation(self, value):
if self.serializer:
return dict(self.serializer(value, context=self.context).data)
else:
return super().to_representation(value)
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
self.to_representation(item)['id'] if self.serializer else self.to_representation(item), # If you end up using another column-name for your primary key, you'll have to change this extraction-key here so it maps the select-element properly.
self.display_value(item)
)
for item in queryset
])
and an example below,
Child Serializer class:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = ChildModel
fields = '__all__'
Parent Serializer Class:
class ParentSerializer(serializers.ModelSerializer):
same_field_name_as_model_foreign_key = NestedKeyField(queryset=ChildModel.objects.all(), serializer=ChildSerializer)
class Meta:
model = ParentModel
fields = '__all__'
Based on the answers of both JPG and Bono, I came up with a solution that handles the OpenAPI Schema generator of DRF as well.
The actual field class is:
from rest_framework import serializers
class ModelRepresentationPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.response_serializer_class = kwargs.pop('response_serializer_class', None)
if self.response_serializer_class is not None \
and not issubclass(self.response_serializer_class, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super(ModelRepresentationPrimaryKeyRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.response_serializer_class else True
def to_representation(self, instance):
if self.response_serializer_class is not None:
return self.response_serializer_class(instance, context=self.context).data
return super(ModelRepresentationPrimaryKeyRelatedField, self).to_representation(instance)
The extended AutoSchema class is:
import inspect
from rest_framework.schemas.openapi import AutoSchema
from .fields import ModelRepresentationPrimaryKeyRelatedField
class CustomSchema(AutoSchema):
def _map_field(self, field):
if isinstance(field, ModelRepresentationPrimaryKeyRelatedField) \
and hasattr(field, 'response_serializer_class'):
frame = inspect.currentframe().f_back
while frame is not None:
method_name = frame.f_code.co_name
if method_name == '_get_request_body':
break
elif method_name == '_get_responses':
field = field.response_serializer_class()
return super(CustomSchema, self)._map_field(field)
frame = frame.f_back
return super(CustomSchema, self)._map_field(field)
Then on your Dganjo's project settings you can define this new Schema class to be used globally like:
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': '<path_to_custom_schema>.CustomSchema',
}
Lastly from within your models you can use the new field type like:
class ExampleSerializer(serializers.ModelSerializer):
test_field = ModelRepresentationPrimaryKeyRelatedField(queryset=Test.objects.all(), response_serializer_class=TestListSerializer)
I have been also stuck in the same situation. But what i have done that i have created two serializers for the following models as follow:
class Base_Location(models.Model):
Base_Location_id = models.AutoField(primary_key = True)
Base_Location_Name = models.CharField(max_length=50, db_column="Base_Location_Name")
class Location(models.Model):
Location_id = models.AutoField(primary_key = True)
Location_Name = models.CharField(max_length=50, db_column="Location_Name")
Base_Location_id = models.ForeignKey(Base_Location, db_column="Base_Location_id", related_name="Location_Base_Location", on_delete=models.CASCADE)
This is my parent serializer
class BaseLocationSerializer(serializers.ModelSerializer):
class Meta:
model = Base_Location
fields = "__all__"
I'm using this serializer only for get request so in response i got data with foreign key also because of nested serializer
class LocationSerializerList(serializers.ModelSerializer): <-- using for get request
Base_Location_id = BaseLocationSerializer()
class Meta:
model = Location
fields = "__all__"
Screenshot of get method request and response in postman
I'm using this serializer only for post request so while sending post request i do not need to include any additional information rather than primary key field value
class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request
class Meta:
model = Location
fields = "__all__"
Screenshot of post method request and response in postman
Here's what I'm using all over. This may be the simplest, most straight forward method which needs no hacks etc, and is directly using DRF without jumping thru hoops. Happy to hear disagreements with this approach.
In the view's perform_create (or equivalent), fetch the FK model database object corresponding to the field sent in the POST request, and then send that into the Serializer. The field in the POST request can be anything that can be used to filter and locate the DB object, need not be an ID.
This is documented here: https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview
These hooks are particularly useful for setting attributes that are
implicit in the request, but are not part of the request data. For
instance, you might set an attribute on the object based on the
request user, or based on a URL keyword argument.
def perform_create(self, serializer):
serializer.save(user=self.request.user)
This method also has the advantage of maintaining parity between the read and write side, by not sending a nested representation for child in the response to the GET or POST.
Given the example posted by the OP:
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# Note this is different from the OP's example. This will send the
# child name in the response
child = serializers.ReadOnlyField(source='child.name')
class Meta:
model = Parent
fields = ('name', 'phone_number', 'child')
In the View's perform_create:
class SomethingView(generics.ListCreateAPIView):
serializer_class = ParentSerializer
def perform_create(self, serializer):
child_name = self.request.data.get('child_name', None)
child_obj = get_object_or_404(Child.objects, name=child_name)
serializer.save(child=child_obj)
PS: Please note that I've not tested this above snippet, however its based on a pattern I'm using in many places so it should work as is.
I'm working on a Django Rest Framework project, in which I have created the following models as:
from django.db import models
# Base Models...
choices = (
('Single', 'Single'),
('Multiple', 'Multiple'),
)
class UserAccountModel(models.Model):
deployment_name = models.CharField(max_length=150, blank=True)
credentials = models.FileField(upload_to='media/credentials/', name='credentials'),
project_name = models.CharField(max_length=150, blank=True)
project_id = models.CharField(max_length=100, blank=False, name='project_id')
cluster_name = models.CharField(max_length=150, blank=False)
zone_region = models.CharField(max_length=150, blank=False)
services = models.CharField(max_length=100, choices=choices)
def __str__(self):
return self.deployment_name
class AwdModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awdSource/', name='awd_source')
routing = models.TextField(name='routing', null=True)
def __str__(self):
return self.deployment_name
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
# def clean(self):
# if self.services == 'Multiple' and self.routing is None:
# raise ValidationError('You must have to provide routing for multiple services deployment.')
class AwodModel(UserAccountModel):
source_zip = models.FileField(upload_to='media/awodSource/', name='awod_source')
routing = models.TextField({'type': 'textarea'}, name='routing')
def save(self, **kwargs):
if not self.id and self.services == 'Multiple' and not self.routing:
raise ValidationError("You must have to provide routing for multiple services deployment.")
super().save(**kwargs)
I need to serialize these models, Here's how I have implemented serializers for these models:
from rest_framework import serializers
from .models import UserAccountModel, AwdModel, AwodModel
class UserAccountSerializer(serializers.ModelSerializer):
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
class AWDSerializer(serializers.ModelSerializer):
class Meta(UserAccountSerializer.Meta):
model = AwdModel
fields = UserAccountSerializer.Meta.fields + ('awd_source', 'routing',)
class AWODSerializer(serializers.ModelSerializer):
class Meta:
model = AwodModel
fields = '__all__'
But, when I try to access, AWDSerialzer it return an error as:
AttributeError at /api/v1/deployments/
Got AttributeError when attempting to get a value for field project_id on serializer AWDSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
Original exception text was: 'QuerySet' object has no attribute 'project_id'.
Update: Here's my APIView code:
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments).data
return Response(data)
class DeploymentDetail(APIView):
def get(self, request, *args, **kwargs):
deployment = get_object_or_404(AwdModel, pk=kwargs['pk'])
data = AWDSerializer(deployment).data
return Response(data)
Help me, please!
Thanks in advance!
AttributeError at /api/v1/deployments/ Got AttributeError when
attempting to get a value for field project_id on serializer
AWDSerializer. The serializer field might be named incorrectly and not
match any attribute or key on the QuerySet instance. Original
exception text was: 'QuerySet' object has no attribute 'project_id'.
This is an attribute error, when attempting to get the value from field project_id .
Get rid of the name attribute in the project_id field.
Edit The APIView code
To serialize a queryset or list of objects instead of a single object
instance, you should pass the many=True flag when instantiating the
serializer. You can then pass a queryset or list of objects to be
serialized. [Serializing multiple objects]
class DeploymentsList(APIView):
def get(self, request):
MAX_OBJECTS = int(20)
deployments = AwdModel.objects.all()[:MAX_OBJECTS]
data = AWDSerializer(deployments, many=True).data
return Response(data)
I hope this will help.
The code that you posted appears to be valid and correct. The issue however is unrelated. The exception text 'QuerySet' object has no attribute 'project_id' Refers to an issue that likely originates from your restframework app's views.py file. The exception states that you are attempting to access the attribute 'project_id' from a QuerySet.
A QuerySet is a (lazy loaded) set of models and not a single model. Even if the query set had only one element you'd still be required to access that element before accessing it's attributes.
Because you haven't shared your views.py file I can't say for sure where the issue is however here is an incorrect use case example: MyModel.objects.all().project_id. Here we can see that I am attempting to access the attribute project_id from a query set. A correct use case would be MyModel.objects.all()[0].project_id. However this assumes that the query set is not empty.
Practically, most DjangoRestFramework views inherit from rest_framework.views.APIView which subclasses django's View Class. I would suggest checking the query_set within that class is being used correctly.
Feel free to share your implementation here for further comment.
[EDIT] - After views.py coded was added.
You are attempting to serializer an entire query set with the instantiation of a serializer data = AWDSerializer(deployments).data this is causing the attribute error.
I would recommend the generics.ListAPIView class and the use of the class attributes query_set and serializer_class. These are simple to implement. You can then invoke the APIViews default get method. Here is an example for your DeploymentsList view
from rest_framework import generics
class DeploymentsList(generics.ListAPIView):
serializer_class = AWDSerializer
queryset = AwdModel.objects.all()
def get(self, request, *args, **kwargs):
MAX_OBJECTS = int(20)
self.queryset = self.queryset[:MAX_OBJECTS]
return super(DeploymentsList, self).get(request, *args, **kwargs)
[EDIT] - FileField Serialization
In order to serialize the UserAccount.credentials file field so that we serializer the path, we can use the serializers.SerializerMethodField. I.e Your UserAccountSerializer becomes:
class UserAccountSerializer(serializers.ModelSerializer):
credentials = serializers.SerializerMethodField()
def get_credentials(self, user_account):
return user_account.credentials.path
class Meta:
model = UserAccountModel
fields = ('deployment_name', 'credentials', 'project_name',
'project_id', 'cluster_name', 'zone_region', 'services')
When you inherit from a model class which is not defined as abstract in it’s own meta class, then Django creates a one-to-one relation between the subclass and its parent. Which actually creates two tables in the database; one for the base class and one for the subclass.
I haven’t tried your code, nor used Django 2, but would check using a relational field between the two serializer.
I have two apps in Django where one app's model (ScopeItem) on its instance creation must create an instance of the other app's model as well (Workflow); i.e. the ScopeItem contains it's workflow.
This works nicely when tried from the shell. Creating a new ScopeItem creates a Workflow and stores it in the ScopeItem. In admin I get an error, that the workflow attribute is required. The attribute is not filled in and the model definition requires it to be set. The overwritten save method though does this. Hence my question is, how to call save before the check in admin happens?
If I pick an existing Workflow instance in admin and save (successfully then), then I can see that my save method is called later and a new Workflow is created and attached to the ScopeItem instance. It is just called too late.
I am aware that I could allow empty workflow attributes in a ScopeItem or merge the ScopeItem and the Workflow class to avoid the issue with admin. Both would cause trouble later though and I like to avoid such hacks.
Also I do not want to duplicate code in save_item. Just calling save from there apparently does not cut it.
Here is the code from scopeitems/models.py:
class ScopeItem(models.Model):
title = models.CharField(max_length=64)
description = models.CharField(max_length=4000, null=True)
workflow = models.ForeignKey(Workflow)
def save(self, *args, **kwargs):
if not self.id:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
super(ScopeItem, self).save(*args, **kwargs)
And workflow/models.py:
from django.utils.timezone import now
class Workflow(models.Model):
PENDING = 0
APPROVED = 1
CANCELLED = 2
STATUS_CHOICES = (
(PENDING, 'Pending'),
(APPROVED, 'Done'),
(CANCELLED, 'Cancelled'),
)
description = models.CharField(max_length=4000)
status = models.IntegerField(choices=STATUS_CHOICES)
approval_date = models.DateTimeField('date approved', null=True)
creation_date = models.DateTimeField('date created')
update_date = models.DateTimeField('date updated')
def save(self, *args, **kwargs):
if not self.id:
self.creation_date = now()
self.update_date = now()
super(Workflow, self).save(*args, **kwargs)
In scopeitems/admin.py I have:
from django.contrib import admin
from .models import ScopeItem
from workflow.models import Workflow
class ScopeItemAdmin(admin.ModelAdmin):
list_display = ('title', 'description', 'status')
list_filter = ('workflow__status', )
search_fields = ['title', 'description']
def save_model(self, request, obj, form, change):
obj.save()
def status(self, obj):
return Workflow.STATUS_CHOICES[obj.workflow.status][1]
admin.site.register(ScopeItem, ScopeItemAdmin)
You could set the field blank=True on workflow.
You said you don't want to allow "empty workflow attributes in a ScopeItem." Setting blank=True is purely validation-related. Thus, on the backend workflow will still be NOT NULL. From the Django docs:
If a field has blank=True, form validation will allow entry of an empty value.
Referring to your example you should be able to use:
workflow = models.ForeignKey(Workflow, blank=True)
You need to exclude the field from the form used in the admin, so that it won't be validated.
class ScopeItemForm(forms.ModelForm):
class Meta:
exclude = ('workflow',)
model = ScopeItem
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
#Daniel Roseman's answer is correct as long as you don't need to edit the workflow field in admin at any time. If you do need to edit it then you'll need to write a custom clean() method on the admin form.
forms.py
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
if 'pk' not in self.instance:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
return cleaned_data
admin.py
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemAdminForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
Answering my own question:
As #pcoronel suggested, the workflow attribute in ScopeItem must have blank=True set to get out of the form in the first place.
Overwriting the form's clean method as suggested by #hellsgate was also needed to create and store the new Workflow.
To prevent code duplication I added a function to workflow/models.py:
def create_workflow(title="N/A"):
workflow = Workflow(
description='ScopeItem %s workflow' % title,
status=Workflow.PENDING)
workflow.save()
return workflow
This makes the ScopeItemAdminForm look like this:
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
cleaned_data['workflow'] = create_workflow(cleaned_data['title'])
return cleaned_data
Additionally I changed the save method in scopeitems/models.py to:
def save(self, *args, **kwargs):
if not self.id:
if not self.workflow:
self.workflow = create_workflow(self.title)
super(ScopeItem, self).save(*args, **kwargs)