I'm struggling to update records with the writeable nested serializers I've created.
There are many Listing categories for a classifieds app I'm creating that each have a few unique attributes, but they also share many attributes. I have a handful of django models that inherit from a parent Listing model, and one of these models, Battery, contains some nested data. So far I've been able to create Battery records but keep getting AttributeErrors when I try to update them.
I've tried to include only the relevant code. Here are my views:
# views.py
class ListingCreateView(CreateAPIView):
queryset = Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
class ListingUpdateView(UpdateAPIView):
queryset = Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
here are my models:
# models.py
## Parent model
class Listing(models.Model):
title = models.CharField(max_length=100)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True, default=0.00)
## One of the child models
class Battery(Listing):
model_name = models.TextField(blank=True, null=True, default="")
color = models.ForeignKey(Color, on_delete=models.CASCADE, blank=True, null=True)
manufacture_year = models.IntegerField(null=True)
## Model for the nested data in Battery model
class Drum(models.Model):
drum_type = models.CharField(max_length=50, blank=True)
size = models.TextField(blank=True, null=True, default="")
battery = models.ForeignKey(Battery, related_name='drums', on_delete=models.CASCADE, null=True)
and here are my serializers:
# serializers.py
class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = Listing
fields = '__all__'
class DrumSerializer(serializers.ModelSerializer):
class Meta:
model = Drum
fields = ['drum_type', 'size', 'carrier', 'stand', 'cover', 'case', 'sold']
class BatterySerializer(serializers.ModelSerializer):
drums = DrumSerializer(many=True)
class Meta:
model = Battery
fields = ['id', 'title', 'description', 'price', 'model_name', 'color', 'manufacture_year', 'drums']
def create(self, validated_data):
drum_data = validated_data.pop('drums')
battery = Battery.objects.create(**validated_data)
for drum_data in drum_data:
Drum.objects.create(battery=battery, **drum_data)
return battery
def update(self, instance, validated_data):
# Update Listing field values
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('description', instance.description)
instance.price = validated_data.get('price', instance.price)
# Grab the Battery record for this Listing and update its values
instance_battery = Battery.objects.get(pk=instance.pk)
instance_battery.model_name = validated_data.get('model_name', instance_battery.model_name)
instance_battery.color = validated_data.get('color', instance_battery.color)
instance_battery.manufacture_year = validated_data.get('manufacture_year', instance_battery.manufacture_year)
# Check for a list of drums
drum_data = validated_data.pop('drums')
# If it exists
if drum_data:
# Clear the existing drums
instance_battery.drums.clear()
# Create new drums
Drum.objects.bulk_create(
[
Drum(**drum)
for drum in drum_data
],
)
# Save the updated Listing & Battery
instance.save()
instance_battery.save()
# Return the updated Battery
return instance
I feel like I've followed the DRF documentation about writeable nested serializers correctly, but I continue to get this AttributeError when I try to post an update to a Battery record:
AttributeError: Got AttributeError when attempting to get a value for field drums on serializer BatterySerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Listing instance.
Original exception text was: 'Listing' object has no attribute 'drums'.
Judging by the error message I think the Django model inheritance requires a more specific solution that what the DRF documentation provides. Can anybody help me understand how to create the serializers I need to create/update a Battery record that inherits from a Listing model with a nested list of Drum records?
Thanks!
I think the problem appears because the view waits for a Listing instance. I can suggest the following: try to redefine def get_qyeryset(self)
For example:
class ListingUpdateView(UpdateAPIView):
def get_queryset(self):
if self.request.data['category'] == 6:
return Battery.objects.all()
else:
return Listing.objects.all()
def get_serializer_class(self):
category = self.request.data['category']
if category == 1:
return PercussionSerializer
elif category == 6:
return BatterySerializer
return ListingSerializer
Maybe it is not the best way, but it can solve your problem
Related
i'm working on a django project and i got this error (Cannot assign "'11'": "Product.category" must be a "CategoryProduct" instance.) anyone here can help me please.
Model:
class Product(models.Model):
name = models.CharField("Nombre", max_length=150)
category = models.ForeignKey(CategoryProduct, on_delete=models.SET_NULL, null=True, related_name='category')
def __str__(self):
return self.name
View:
class ProductCreateView(CreateView):
model = Product
form_class = ProductForm
success_url = '/adminpanel/products/'
def post(self, request, *args, **kwargs):
form = self.get_form()
category = CategoryProduct.objects.get(id=request.POST['category'])
if form.is_valid():
product = form.save(commit=False)
product.category = category
product.save()
Form:
class ProductForm(forms.ModelForm):
name = forms.CharField(max_length=150, label="Nombre")
category = forms.ChoiceField(choices=[(obj.id, obj.name) for obj in CategoryProduct.objects.all()], label="Categoría")
class Meta:
model = Product
fields = ['name', 'category']
You can let Django's ModelForm do its work, this will create a ModelChoiceField [Django-doc], which is where the system gets stuck: it tries to assign the primary key to category, but that should be a ProductCategory object, so you can let Django handle this with:
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'category']
If you want to specify a different label, you can use the verbose_name=… [Django-doc] from the model field, or specify this in the labels options [Django-doc] of the Meta of the ProductForm. So you can specify Categoria with:
class Product(models.Model):
name = models.CharField('Nombre', max_length=150)
category = models.ForeignKey(
CategoryProduct,
on_delete=models.SET_NULL,
null=True,
related_name='products',
verbose_name='Categoria'
)
def __str__(self):
return self.name
then the CreateView can just use its boilerplate logic:
class ProductCreateView(CreateView):
model = Product
form_class = ProductForm
success_url = '/adminpanel/products/'
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the Category model to the Product
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the category relation to products.
Let us imagine that I have two models.
First model contains curse details and user that created this course
class Course(models.Model):
course_name = models.CharField(max_length=100, null=False)
description = models.CharField(max_length=255)
user_profile = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
and my second model is:
class Lesson(models.Model):
course = models.OneToOneField(Course, on_delete=models.CASCADE) #
# inside the course I want my APIVIEW to list only the courses that current user created.
# OnetoOne relationship does not solve the problem.
status = models.CharField(choices=STATUS, null=False, default=GOZLEMEDE,max_length=20)
tariffs = models.FloatField(max_length=5,null=False,default=0.00)
continues_off = models.CharField(max_length=2)
user_profile = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
My serializers for both Models:
class LessonSerializer(serializers.ModelSerializer):
class Meta:
model = models.Lesson
fields = ('course', 'status', 'tariffs', 'continues_off', 'user_profile')
def create(self, validated_data):
lesson = models.Lesson.objects.create(
course = validated_data['course'],
status = validated_data['status'],
tariffs=validated_data['tariffs'],
continues_off=validated_data['continues_off'],
user_profile=validated_data['user_profile']
)
return lesson
class CourseSerializer(serializers.ModelSerializer):
"""Serializers Course content"""
class Meta:
model = models.Course
fields = '__all__'
def create(self,validated_data):
course = models.Course.objects.create(
course_name = validated_data['course_name'],
description=validated_data['description'],
user_profile=validated_data['user_profile']
)
return course
My Viewset:
class LessonViewset(viewsets.ModelViewSet):
model = models.Lesson
serializer_class = serializers.LessonSerializer
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,BasePermission,)
def get_queryset(self):
user_current = self.request.user.id
return models.Lesson.objects.filter(user_profile=user_current)
How can I get the desired result. I want to get the courses for the current user and show them as a dropdown list in my API view. Just only the courses that user created should be in the dropdown list not all.
OnetoOne relationship gives all results of course table.
i think change your view code to :
def get_queryset(self,id):
return model.objects.filter(user_profile=id)
#You do not need to call it again when you put the Lesson on the model
\
I am Django rest framework to return the list of objects who do not have a foreign key in another table. what queryset should I write to get those objects.
models.py
class Event(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=100,default='')
description = models.TextField(blank=True,default='', max_length=1000)
link = models.URLField(null=True)
image = models.ImageField(null=True, blank=True)
organizer = models.CharField(max_length=100, default='')
timings = models.DateTimeField(default=None)
cost = models.IntegerField(default=1,null=True,blank=True)
def __str__(self):
return self.title
class Featured(models.Model):
event = models.ForeignKey(Event, null=True ,on_delete=models.PROTECT, related_name="event")
def __str__(self):
return self.event.title
class Meta:
verbose_name_plural = 'Featured'
views.py
class Upcoming2EventsViewSet(mixins.RetrieveModelMixin,mixins.ListModelMixin,viewsets.GenericViewSet):
serializer_class = Upcoming2Events
def get_queryset(self):
featured_events = Featured.objects.all().values_list('id')
return Event.objects.filter(id__in=featured_events)
# return Event.objects.exclude(id__in=featured_events.event.id)
# # return Event.objects.exclude(id__in = [featured_events.id])
serializers.py
class Upcoming2Events(serializers.ModelSerializer):
id = serializers.CharField(source='event.id')
title = serializers.CharField(source='event.title')
timings = serializers.DateTimeField(source='event.timings')
organizer = serializers.CharField(source='event.organizer')
class Meta:
model = Featured
fields = ['id','title','organizer','timings']
Error
Got AttributeError when attempting to get a value for field `id` on serializer `Upcoming2Events`.
The serializer field might be named incorrectly and not match any attribute or key on the `Event` instance.
Original exception text was: 'RelatedManager' object has no attribute 'id'.
Can you tell me what queryset should I write to get the only objects which are not present in the table Featured?
Also, what should I do to get only the upcoming 2 events from the Event table which are not present in the Featured table?
Note I am not supposed to use any flag value, can you provide some other solutions?
Based on the Informations you wrote here, i would suggest using a flag to determine a featured event. A second Model is useful if you want to provide more Informations on this specific for a featured event
like this:
class Event(models.Model):
id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=100,default='')
description = models.TextField(blank=True,default='', max_length=1000)
link = models.URLField(null=True)
image = models.ImageField(null=True, blank=True)
organizer = models.CharField(max_length=100, default='')
timings = models.DateTimeField(default=None)
cost = models.IntegerField(default=1,null=True,blank=True)
featured = models.BooleanField(default=False)
so you can directly use querysets to get what you want:
Event.objects.exclude(featured=True)
Event.objects.exclude(featured=True).order_by('-timings')[:2]
I would use ModelViewsets directly, hence you will use your model here.
views and serializers would look like this:
views.py
class Upcoming2EventsViewSet(viewesets.ReadyOnlyModelViewSet):
serializer_class = EventSerializer
queryset = Event.objects.exclude(featured=True).order_by('-timings')[:2]
serializers.py
class EventSerializer(serializers.ModelSerilizer):
class Meta:
model = Event
fields = ['id', 'title', 'organizer', 'timings']
As improvement i would provide filters instead of setting up different ViewSets for just filtering querysets.
I've got the following Situation, I have a rather large legacy model (which works nonetheless well) and need one of its fields as a distinct dropdown for one of my forms:
Legacy Table:
class SummaryView(models.Model):
...
Period = models.CharField(db_column='Period', max_length=10, blank=True, null=True)
...
def __str__(self):
return self.Period
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'MC_AUT_SummaryView'
Internal Model:
class BillCycle(models.Model):
...
Name = models.CharField(max_length=100, verbose_name='Name')
Period = models.CharField(max_length=10, null=True, blank=True)
Version = models.FloatField(verbose_name='Version', default=1.0)
Type = models.CharField(max_length=100, verbose_name='Type', choices=billcycle_type_choices)
Association = models.ForeignKey(BillCycleAssociation, on_delete=models.DO_NOTHING)
...
def __str__(self):
return self.Name
Since I don't want to connect them via a Foreign Key (as the SummaryView is not managed by Django) I tried a solution which I already used quite a few times. In my forms I create a ModelChoiceField which points to my Legacy Model:
class BillcycleModelForm(forms.ModelForm):
period_tmp = forms.ModelChoiceField(queryset=SummaryView.objects.values_list('Period', flat=True).distinct(),
required=False, label='Period')
....
class Meta:
model = BillCycle
fields = ['Name', 'Type', 'Association', 'period_tmp']
And in my view I try to over-write the Period Field from my internal Model with users form input:
def billcycle_create(request, template_name='XXX'):
form = BillcycleModelForm(request.POST or None)
data = request.POST.copy()
username = request.user
print("Data:")
print(data)
if form.is_valid():
initial_obj = form.save(commit=False)
initial_obj.ModifiedBy = username
initial_obj.Period = form.cleaned_data['period_tmp']
initial_obj.Status = 'Creating...'
print("initial object:")
print(initial_obj)
form.save()
....
So far so good:
Drop Down is rendered correctly
In my print Statement in the View ("data") I see that the desired infos are there:
'Type': ['Create/Delta'], 'Association': ['CP'], 'period_tmp': ['2019-12']
Still I get a Select a valid choice. That choice is not one of the available choices. Error in the forms. Any ideas??
I'm trying to create a FormView that receives a string but it gives me a "objects already exists" error when I complete the field I give. What I'm trying to do is to create a view that checks if a certain "product" (model) exists, if that product really exists, redirect to another view based on the product "pk" to create another model.
Basically the course of action is like this:
Check if product exists.
if exists redirect to create order (model) view, else no nothing.
Fill the create order form, if valid, create the order and assign the product fk relation to order.
Here's my code
views.py
class BuyOrderCheckProduct(generic.FormView):
template_name = 'buy_order/buy_order_check_product.html'
form_class = forms.CheckProductForm
def form_valid(self, form):
try:
product = Product.objects.get(codename=form.cleaned_data['codename'])
except Product.DoesNotExist:
product = None
if product:
# Never enters here because correct existing codename gives form_invalid, don't know why
return super(BuyOrderCheckProduct, self).form_valid()
else:
# It only enters when I input a non-existent codename for product
return super(BuyOrderCheckProduct, self).form_invalid()
def form_invalid(self, form):
# I don't know why it enters here!
return super(BuyOrderCheckProduct, self).form_invalid()
def get_success_url(self, **kwargs):
# TODO: How to pass product pk as kwargs?
return reverse_lazy('order_create', self.kwargs['pk'])
class BuyOrderCreate(generic.CreateView):
template_name = 'buy_order/buy_order_create.html'
form_class = forms.BuyOrderCreateForm
success_url = reverse_lazy('buy_order_list')
# TODO: Need to create a custom form_valid to add product fk to order.
forms.py
class CheckProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['codename']
class BuyOrderCreateForm(forms.ModelForm):
class Meta:
model = BuyOrder
models.py
"""
ORDER
"""
class Order(models.Model):
class Meta:
verbose_name = u'orden'
verbose_name_plural = u'ordenes'
abstract = True
unit_price = models.IntegerField(u"precio unitario", )
quantity = models.IntegerField(u"cantidad", default=1)
discount = models.IntegerField(u"descuento")
def __unicode__(self):
return self.code
class BuyOrder(Order):
class Meta:
verbose_name = u'orden de compra'
verbose_name_plural = u'ordenes de compra'
product = models.ForeignKey(Product, related_name="buy_orders", editable = False)
bill = models.ForeignKey(BuyBill, related_name="orders", null=True, editable = False)
"""
PRODUCT
"""
class Product(models.Model):
class Meta:
verbose_name = u'producto'
verbose_name_plural = u'productos'
category = models.ForeignKey(Category, verbose_name=u'categoría', related_name='products')
codename = models.CharField(u"código", max_length=100, unique=True)
name = models.CharField(u"nombre", max_length=100)
description = models.TextField(u"descripción", max_length=140, blank=True)
sale_price = models.IntegerField(u"precio de venta", default=0)
purchase_price = models.IntegerField(u"precio de compra", default=0)
profit = models.IntegerField(u"lucro", default=0)
profit_margin = models.IntegerField(u"margen de lucro", default=0)
tax = models.IntegerField(u"tasa", default=0)
quantity = models.IntegerField(u"cantidad", default=0)
picture = models.ImageField(u"imagen", upload_to='product_pictures', blank=True)
group = models.ForeignKey(Group, verbose_name=u'grupo', related_name='products')
def __unicode__(self):
return self.name
I'll be appreciated if you give me a tip for creating a correct get_success_url() for this case.
Ok. I found a solution for my error. What caused the model already exists error was my ModelForm CheckProductForm. Codename attribute is unique, so my validation always returned False. What I did was to change my orginal ModelForm to a Form. This solved my whole issue. And for the form_invalid in form_valid issue. I've overwritten my form's clean_codename function to raise ValidationError if product doesn´t exist.
Here's the solution I found:
views.py
class BuyOrderCheckProduct(generic.FormView):
template_name = 'buy_order/buy_order_check_product.html'
form_class = forms.CheckProductForm
def form_valid(self, form):
product = Product.objects.get(codename=form.cleaned_data['codename'])
return redirect('buy_order_create', pk=product.pk)
forms.py
class CheckProductForm(forms.Form):
codename = forms.CharField(label=u'código')
def clean_codename(self):
try:
product = Product.objects.get(codename=self.cleaned_data['codename'])
except Product.DoesNotExist:
raise forms.ValidationError("This codename doesn't exist.")
return product
PD: Sorry for the dumb questions.