What should my django serializer def Update look like? - django

I'm trying to add content for the first time using a DRF back-end written by someone else. I am receiving this error...
django_1 | AssertionError: The `.update()` method does not support writable nested fields by default.
django_1 | Write an explicit `.update()` method for serializer `myapp.tracker.serializers.MedicationSerializer`, or set `read_only=True` on nested serializer fields.
How can I write the Update method?
I've done rails before so I'm familiar with the concepts? So when I see a method "create" in my serilaizers.py I think "there must be a way to write a def Update here" But since I'm new to django I have NO IDEA what that method should actually look like ^_^. This is where I'm stuck.
Here is the serializers.py , models.py, and views.py code specific to the model I am trying to update...let me know if I need to post another files contents.
What should my def update method look like?
my serializer.py
class MedicationSerializer(serializers.HyperlinkedModelSerializer):
cat = CatSerializer()
class Meta:
model = Medication
fields = (
'id',
'cat',
'name',
'duration',
'frequency',
'dosage_unit',
'dosage',
'notes',
'created',
'modified',
'showRow',
)
def create(self, validated_data):
cat_data = validated_data.pop('cat')
cat_obj = Cat.objects.get(**cat_data)
medication = Medication.objects.create(cat=cat_obj, **validated_data)
return medication
the models.py looks like this
class Medication(models.Model):
cat = models.ForeignKey(Cat, blank=True, null=True)
name = models.CharField(max_length=100)
duration = models.TextField(blank=True, null=True)
frequency = models.CharField(max_length=2)
dosage_unit = models.CharField(max_length=2, default=Weight.MILLILITERS)
dosage = models.IntegerField(blank=True, null=True)
notes = models.CharField(max_length=2048, blank=True, null=True)
created = models.DateTimeField(blank=True, null=True)
modified = models.DateTimeField(blank=True, null=True)
showRow = models.BooleanField(default=True)
def save(self, *args, **kwargs):
# Save time Medication object modified and created times
self.modified = datetime.datetime.now()
if not self.created:
self.created = datetime.datetime.now()
super(Medication, self).save(*args, **kwargs)
def __str__(self):
if self.cat:
cat_name = self.cat.name
else:
cat_name = "NO CAT NAME"
return "{cat}: {timestamp}".format(cat=self.cat.name, timestamp=self.created)
and the views.py ....
class MedicationViewSet(viewsets.ModelViewSet):
queryset = Medication.objects.all()
serializer_class = MedicationSerializer
filter_fields = ('cat__slug', 'cat__name')
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
Thank you for your time.

Try adding read-only=True to your Catserializer
class MedicationSerializer(serializers.HyperlinkedModelSerializer):
cat = CatSerializer(read_only=True)
class Meta:
model = Medication
fields = (
'id',
'cat',
'name',
'duration',
'frequency',
'dosage_unit',
'dosage',
'notes',
'created',
'modified',
'showRow',
)
def create(self, validated_data):
cat_data = validated_data.pop('cat')
cat_obj = Cat.objects.get(**cat_data)
medication = Medication.objects.create(cat=cat_obj, **validated_data)
return medication

Related

ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance

I've been trying to create an api endpoint to update my "lead" objects and add a list of facilities to them when sending a put request (each time a different amount of facilities). The lead objects already exist inside the database so do the facility objects. Since i need a date and time associated to each facility when they are being added to a lead i created the "LeadFacilityAssign" class.
Since i wasn't able to get it to work i tried to do it just with a post request for now, during the lead creation process. I was told that i need to use bulk_create if i need to add more than one facility this way. I couldn't find anything on bulk_create inside the drf documentation so i decided to do this for now just with one facility and improve my code from there one issue at a time since i'm new to drf.
Does anyone know what is causing this error? I tried a few different things but nothing worked so far.
ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance.
serializers.py
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
models.py
class Facility(models.Model):
name = models.CharField(max_length=150, null=True, blank=False)
def __str__(self):
return self.name
class Lead(models.Model):
first_name = models.CharField(max_length=40, null=True, blank=True)
last_name = models.CharField(max_length=40, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class LeadFacilityAssign(models.Model):
assigned_facilities = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name='leadfacility')
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='leadfacility')
datetime = models.DateTimeField()
views.py
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(agent=self.request.user)
class LeadUpdateView(UpdateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def get_queryset(self):
return Lead.objects.all()
You are trying to add Integer value into FK field.
You have 2 options. You can change the serializer field.
assigned_facilities = serializers.PrimaryKeyRelatedField(queryset=Facility.objects.all(), required=True, write_only=True)
OR
assigned_facilities = serializers.IntegerField(required=True, write_only=True)
instance.leadfacility.create(assigned_facilities_id=assigned_facilities,datetime=datetime)
I would rather use 1 option.
Another potential solution you can apply:
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def validate_assigned_facility(self, facility_pk)->:
assigned_facility = Facility.objects.filter(pk=facility_pk).first()
if assigned_facility:
return assigned_facility
raise ValidationError('Facility not found, provide a valid pk')
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
This solution is kind of big but is so flexible 'cause give you the opportunity to add more business logic around the input and the expected data in the model or datasource.

Is there a way to reload ViewSet on each GET request for new data in DRF?

I am trying to generate a random object from my Model. The problem is that it will only work one time, then I have to restart the server to get a new object. It just keeps giving me the same object until the restart.
I have been looking for solution on stack overflow but haven't found any.
Views.py
def dailyaskist(category):
qs = Task.objects.filter(category=category)
max_num = len(qs)
while True:
pk = random.randint(1, max_num)
task = Task.objects.filter(pk=pk).first()
if task:
return task.pk
class DailyTaskEcommerceViewSet(viewsets.ModelViewSet):
category = 'ecommerce'
task_pk = dailyaskist(category)
queryset = Task.objects.filter(pk=task_pk)
serializer_class = TaskSerializer
serialisers.py
class StepSerializer(serializers.HyperlinkedModelSerializer):
task_id = serializers.PrimaryKeyRelatedField(queryset=Task.objects.all(), source='task.id')
class Meta:
model = Step
fields = ('title', 'description', 'done', 'task_id')
class TaskSerializer(serializers.HyperlinkedModelSerializer):
steps = StepSerializer(many=True, read_only=True)
class Meta:
model = Task
fields = ('title', 'description', 'video', 'done', 'steps')
models.py
Categories = (
('ecommerce', 'Ecommerce'),
)
class Task(models.Model):
title = models.CharField(max_length=50)
description = models.TextField(max_length=360)
video = models.CharField(max_length=30, default='')
category = models.CharField(choices=Categories, default='', max_length=30)
done = models.BooleanField(default=False)
def __str__(self):
return self.title
class Step(models.Model):
task = models.ForeignKey(Task, related_name='steps', on_delete=models.CASCADE)
title = models.CharField(max_length=50)
description = models.TextField(max_length=360)
done = models.BooleanField(default=False)
def __str__(self):
return self.title
I want to receive a new object (task) each time I make a GET request using the DailyTaskEcommerceViewSet.
Thanks in advance! :D
You would do this in a method. In this case, get_queryset seems the right place.
class DailyTaskEcommerceViewSet(viewsets.ModelViewSet):
serializer_class = TaskSerializer
category = 'ecommerce'
def get_queryset(self):
task_pk = dailyaskist(self.category)
return Task.objects.filter(pk=task_pk)

The `.create()` method does not support writable nested fields by default

I am new to Django REST, I was trying to make some entry to the DB using the serilaizer in django rest. But i am getting some errors while using the create method.
My models are,
class CoreInformation(models.Model):
site_name = models.CharField(max_length=145, blank=True, null=True)
status = models.CharField(max_length=45, blank=True, null=True)
created_at = models.DateTimeField(blank=True, null=True, auto_now_add=True)
class Meta:
managed = False
db_table = 'core_information'
class CoreDetailInformation(models.Model):
core_info = models.ForeignKey('CoreInformation', models.DO_NOTHING, related_name='core_base_info')
old_sac = models.CharField(max_length=45, blank=True, null=True)
msc = models.CharField(max_length=45, blank=True, null=True)
class Meta:
db_table = 'core_detail_information'
And i have two ModelSerializer like below ,
class CoreDetailSerializer(serializers.ModelSerializer):
class Meta:
model = CoreDetailInformation
fields = ('id','old_sac', 'msc')
class CustomCoreInfoSerializer(serializers.ModelSerializer):
core_base_info = CoreDetailSerializer(many=True)
class Meta:
model = CoreInformation
# fields = '__all__'
fields = ('id', 'site_name', 'status', 'created_at', 'core_base_info')
#transaction.atomic
def create(self, validated_data):
try:
with transaction.atomic():
base_info = CoreInformation.objects.create(site_name=validated_data['site_name'],status=validated_data['status']
for site_detail in validated_data['core_base_info']:
CoreDetailInformation.objects.get_or_create(msc=site_detail['msc'],old_sac=site_detail['old_sac'],core_info=base_info)
except CoreInformation.DoesNotExist as e:
raise e
except CoreDetailInformation.DoesNotExist as e:
raise e
and my views.py is ,
class CoreInformation(generics.ListCreateAPIView):
queryset = models.CoreInformation.objects.all()
serializer_class = CustomCoreInfoSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_create(serializer)
serializer = self.get_serializer(instance=instance)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def perform_create(self, serializer):
return serializer.create(validated_data=serializer.validated_data)
To create the CoreInformation my input will be like below,
{
"site_name": "xxxxxxxxxx",
"status": "create",
"core_base_info": [{
"old_sac": '1',
"msc": "abc1,abc2"
},
{
"old_sac": '2',
"msc": "abc3,abc4"
}]
}
But when i am compiling its returns me the below error,
AssertionError at /api/core/
The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `src.core.serializers.CustomCoreInfoSerializer`, or set `read_only=True` on nested serializer fields.
I found this , but did n't help for me.
Any help would be greatly appreciated. Thanks.
I think that you can use this GitHub to solve your problem
Try this: https://github.com/beda-software/drf-writable-nested

Django - Design choices, Override the model save() so it palys nice with contrib.admin?

I'm with some design issues in Django and getting all to play nice with contrib.admin.
My main problem is with Admin Inlines and the save_formset() method. I created a create() classmethod for the model but this do not play nice with save_formset(). I think Django admin have a way of doing this, not with a create() method. In the create() method in the AdPrice I basicaly want to update the field 'tags' in the model Ad.
My question: Instead of creating a create() classmethod it would be nice to override the model save() method so I don't have problems with contrib.admin?
My code:
Models:
class Ad(models.Model):
title = models.CharField(max_length=250)
name = models.CharField(max_length=100)
description = models.TextField()
tags = TaggableManager()
comment = models.TextField(null=True, blank=True)
user_inserted = models.ForeignKey(User, null=True, blank=True, related_name='user_inserted_ad')
date_inserted = models.DateTimeField(auto_now_add=True)
user_updated = models.ForeignKey(User, null=True, blank=True, related_name='user_updated_ad')
date_updated = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.name
class AdPrice(models.Model):
ad = models.ForeignKey(Ad)
name = models.CharField(max_length=50)
price = models.DecimalField(max_digits=6, decimal_places=2)
user_inserted = models.ForeignKey(User, null=True, blank=True, related_name='user_inserted_ad_price')
date_inserted = models.DateTimeField(auto_now_add=True)
user_updated = models.ForeignKey(User, null=True, blank=True, related_name='user_updated_ad_price')
date_updated = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.name
#classmethod
def create(cls, ad_id, name, price, date_inserted, user_inserted_id):
# Save price
new_register = AdPrice(ad_id=ad_id, name=name, price=price, date_inserted=date_inserted,
user_inserted=User.objects.get(id=user_inserted_id))
new_register.save()
# Add tags to Ad tags field
# AD SOME CODE HERE # To do
Admin:
class AdPriceInline(admin.TabularInline):
model = AdPrice
fieldsets = (
(None, {
'fields': ('name', 'price')
}),
)
class AdAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'title', 'telephone', 'comment',
'user_inserted', 'date_inserted', 'user_updated', 'date_updated')
fieldsets = (
(None, {
'fields': ('id', 'name', 'title', 'description', 'comment')
}),
)
inlines = (
AdPriceInline,
)
readonly_fields = ('id',)
def save_model(self, request, obj, form, change):
if change == False:
if getattr(obj, 'user_inserted', None) is None:
obj.user_inserted = request.user
super(AdAdmin, self).save_model(request, obj, form, change) # This line save the data on the Ad Model, now I have the pk to use bellow
obj.save()
# In the first insert, create a line in the AdHist model
# ad_status_id = 1 (Pending) | ad_change_reasons = 1(Insertion)
AdHist.create(ad_id=obj.id, datetime_begin=datetime.datetime.now(), datetime_end=None, ad_status_id=1, ad_change_reason_id=1,
user_inserted_id=request.user.id)
elif change == True:
if getattr(obj, 'user_updated', None) is None:
obj.user_updated = request.user
else:
obj.user_updated = request.user
if getattr(obj, 'date_updated', None) is None:
obj.date_updated = datetime.datetime.now()
else:
obj.date_updated = datetime.datetime.now()
obj.save()
def save_formset(self, request, form, formset, change):
if change == False:
instances = formset.save(commit=False)
for instance in instances:
if formset.model == AdPrice:
AdPrice.create(ad_id=instance.ad_id, name=instance.name, price=instance.price, date_inserted=datetime.datetime.now(),
user_inserted_id=request.user.id)
elif change == True:
for form in formset.forms:
None #form.instance.ad_id
formset.save()
A tricky part about overriding a model's save() method is that it doesn't get used in bulk operations, like bulk_create. Django recommends using a signal for a case like this (see the docs on how to set them up in your app).
Your signal might look something like this:
from django.db.models.signals import post_save
from .models import YourModel
def handle_model_post_save(sender, **kwargs):
# Ensure we are only running this code on create
if kwargs['created'] and kwargs['instance']:
# Your logic goes here
With this, the code in the signal will only run when a new instance of your model is created, not when existing instances are updated.

'form.is_valid()' fails when I want to save form with choices based on two models

I want to let users to choose their countries. I have 2 models = Countries with some figures and CountriesTranslations. I am trying to make tuple with country (because user has FK to this model) and its translation. In front-end I see dropdown list of countries, but when I try to save the form, I see
error: Exception Value: Cannot assign "'AF'": "UserProfile.country" must be a "Countries" instance.
Error happens at the line if user_profile_form.is_valid():
# admindivisions.models
class Countries(models.Model):
osm_id = models.IntegerField(db_index=True, null=True)
status = models.IntegerField()
population = models.IntegerField(null=True)
iso3166_1 = models.CharField(max_length=2, blank=True)
iso3166_1_a2 = models.CharField(max_length=2, blank=True)
iso3166_1_a3 = models.CharField(max_length=3, blank=True)
class Meta:
db_table = 'admindivisions_countries'
verbose_name = 'Country'
verbose_name_plural = 'Countries'
class CountriesTranslations(models.Model):
common_name = models.CharField(max_length=81, blank=True, db_index=True)
formal_name = models.CharField(max_length=100, blank=True)
country = models.ForeignKey(Countries, on_delete=models.CASCADE, verbose_name='Details of Country')
lang_group = models.ForeignKey(LanguagesGroups, on_delete=models.CASCADE, verbose_name='Language of Country',
null=True)
class Meta:
db_table = 'admindivisions_countries_translations'
verbose_name = 'Country Translation'
verbose_name_plural = 'Countries Translations'
# profiles.forms
class UserProfileForm(forms.ModelForm):
# PREPARE CHOICES
country_choices = ()
lang_group = Languages.objects.get(iso_code='en').group
for country in Countries.objects.filter(status=1):
eng_name = country.countriestranslations_set.filter(lang_group=lang_group).first()
if eng_name:
country_choices += ((country, eng_name.common_name),)
country_choices = sorted(country_choices, key=lambda tup: tup[1])
country = forms.ChoiceField(choices=country_choices, required=False)
class Meta:
model = UserProfile()
fields = ('email', 'email_privacy',
'profile_url',
'first_name', 'last_name',
'country',)
# profiles.views
def profile_settings(request):
if request.method == 'POST':
user_profile_form = UserProfileForm(request.POST, instance=request.user)
if user_profile_form.is_valid():
user_profile_form.save()
messages.success(request, _('Your profile was successfully updated!'))
return redirect('settings')
else:
messages.error(request, _('Please correct the error below.'))
else:
user_profile_form = UserProfileForm(instance=request.user)
return render(request, 'profiles/profiles_settings.html', {
'user_profile_form': user_profile_form,
})
As I understand, country from ((country, eng_name.common_name),) is converted to str. What is the right way to keep country instance in the form? or if I am doing it in the wrong way, what way is correct?
EDITED:
As a possible solution is to use ModelChoiceField with overriding label_from_instance as shown below:
class CountriesChoiceField(forms.ModelChoiceField):
def __init__(self, user_lang='en', *args, **kwargs):
super(CountriesChoiceField, self).__init__(*args, **kwargs)
self.user_lang = user_lang
def label_from_instance(self, obj):
return obj.countriestranslations_set.get(lang_group=self.user_lang)
class UserProfileForm(forms.ModelForm):
user_lang = user_lang_here
country = CountriesChoiceField(
queryset=Countries.objects.filter(
status=1, iso3166_1__isnull=False,
countriestranslations__lang_group=user_lang).order_by('countriestranslations__common_name'),
widget=forms.Select(), user_lang=user_lang)
class Meta:
model = UserProfile()
fields = ('email', 'email_privacy',
'profile_url',
'first_name', 'last_name',
'country',)
but this solution produces too much queries because of the query in label_from_instance and page loads too slowly. Would appreciate any advice.
You probably want to use forms.ModelChoiceField instead of the forms.ChoiceField for your dropdown list.
The ModelChoiceField builds based on a QuerySet and preserves the model instance.
Seems to be solved.
The version below produces 7 queries in 29.45ms vs 73 queries in 92.52ms in the EDITED above. I think it is possible to make it even faster if to set unique_together for some fields.
class CountriesChoiceField(forms.ModelChoiceField):
def __init__(self, user_lang, *args, **kwargs):
queryset = Countries.objects.filter(
status=1, iso3166_1__isnull=False,
countriestranslations__lang_group=user_lang).order_by('countriestranslations__common_name')
super(CountriesChoiceField, self).__init__(queryset, *args, **kwargs)
self.translations = OrderedDict()
for country in queryset:
name = country.countriestranslations_set.get(lang_group=user_lang).common_name
self.translations[country] = name
def label_from_instance(self, obj):
return self.translations[obj]
class UserProfileForm(forms.ModelForm):
user_lang = user_lang_here
country = CountriesChoiceField(widget=forms.Select(), user_lang=user_lang)
class Meta:
model = UserProfile()
fields = ('email', 'email_privacy',
'profile_url',
'first_name', 'last_name',
'country',)
So, now it is possible to have choices based on two (2) models with a good speed. Also the DRY principle is applied, so if there is a need to use choices multiple times in different forms - no problem.