Django Admin Inline hit database multiple times - django

DjangoVersion:2.1.7
PythonVersion:3.8.2
I have a StoreLocation model which has ForeignKeys to Store, City and Site (this is the default DjangoSitesFramework model). I have created TabularInline for StoreLocation and added it to the Store admin. everything works fine and there isn't any problem with these basic parts.
now I'm trying to optimize my admin database queries, therefore I realized that StoreLocationInline will hit database 3 times for each StoreLocation inline data.
I'm using raw_id_fields, managed to override get_queryset and select_related or prefetch_related on StoreLocationInline, StoreAdmin and even in StoreLocationManager, i have tried BaseInlineFormsSet to create custom formset for StoreLocationInline. None of them worked.
Store Model:
class Store(models.AbstractBaseModel):
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
City Model:
class City(models.AbstractBaseModel, models.AbstractLatitudeLongitudeModel):
province = models.ForeignKey('locations.Province', on_delete=models.CASCADE, related_name='cities')
name = models.CharField(max_length=200)
has_shipping = models.BooleanField(default=False)
class Meta:
unique_together = ('province', 'name')
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
StoreLocation model with manager:
class StoreLocationManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('store', 'city', 'site')
class StoreLocation(models.AbstractBaseModel):
store = models.ForeignKey('stores.Store', on_delete=models.CASCADE, related_name='locations')
city = models.ForeignKey('locations.City', on_delete=models.CASCADE, related_name='store_locations')
site = models.ForeignKey(models.Site, on_delete=models.CASCADE, related_name='store_locations')
has_shipping = models.BooleanField(default=False)
# i have tried without manager too
objects = StoreLocationManager()
class Meta:
unique_together = ('store', 'city', 'site')
def __str__(self):
return '[{}] {} / {} / {}'.format(self.id, self.store.name, self.city.name, self.site.name)
# removing shipping_status property does not affect anything for this problem.
#property
def shipping_status(self):
return self.has_shipping and self.city.has_shipping
StoreLocationInline with custom FormSet:
class StoreLocationFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queryset = self.queryset.select_related('site', 'city', 'store')
class StoreLocationInline(admin.TabularInline):
model = StoreLocation
# i have tried without formset and multiple combinations too
formset = StoreLocationFormset
fk_name = 'store'
extra = 1
raw_id_fields = ('city', 'site')
fields = ('is_active', 'has_shipping', 'site', 'city')
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).select_related('site', 'city', 'store')
StoreAdmin:
class StoreAdmin(AbstractBaseAdmin):
list_display = ('id', 'name') + AbstractBaseAdmin.default_list_display
search_fields = ('id', 'name')
inlines = (StoreLocationInline,)
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('locations', 'locations__city', 'locations__site')
so when i check Store admin change form, it will do this for each StoreLocationInline item:
DEBUG 2020-04-13 09:32:23,201 [utils:109] (0.000) SELECT `locations_city`.`id`, `locations_city`.`is_active`, `locations_city`.`priority`, `locations_city`.`created_at`, `locations_city`.`updated_at`, `locations_city`.`latitude`, `locations_city`.`longitude`, `locations_city`.`province_id`, `locations_city`.`name`, `locations_city`.`has_shipping`, `locations_city`.`is_default` FROM `locations_city` WHERE `locations_city`.`id` = 110; args=(110,)
DEBUG 2020-04-13 09:32:23,210 [utils:109] (0.000) SELECT `django_site`.`id`, `django_site`.`domain`, `django_site`.`name` FROM `django_site` WHERE `django_site`.`id` = 4; args=(4,)
DEBUG 2020-04-13 09:32:23,240 [utils:109] (0.000) SELECT `stores_store`.`id`, `stores_store`.`is_active`, `stores_store`.`priority`, `stores_store`.`created_at`, `stores_store`.`updated_at`, `stores_store`.`name`, `stores_store`.`description` FROM `stores_store` WHERE `stores_store`.`id` = 22; args=(22,)
after i have added store to the select_related, the last query for store disappeared. so now i have 2 queries for each StoreLocation.
I have checked the following questions too:
Django Inline for ManyToMany generate duplicate queries
Django admin inline: select_related
Problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value()
as #AndreyKhoronko mentioned in comments, this problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value() located in django.contrib.admin.widgets which will hit database with that key to generate a link to admin change form.
class ForeignKeyRawIdWidget(forms.TextInput):
# ...
def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist, ValidationError):
return '', ''
try:
url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
obj._meta.object_name.lower(),
),
args=(obj.pk,)
)
except NoReverseMatch:
url = '' # Admin not registered for target model.
return Truncator(obj).words(14, truncate='...'), url

Related

Django Admin formfield_for_foreignkey for manytomany

My Model (simpliefied)
we have tours:
class Tour(models.Model):
name = models.CharField(max_length=100)
strecke = models.ManyToManyField(Strecke, through="Streckenreihenfolge")
A tour has sections:
class Strecke(models.Model):
name = models.CharField(max_length=100)
auftrag = models.ForeignKey("Auftrag", on_delete=models.CASCADE, null=True, blank=True)
And the sections are put in order
class Streckenreihenfolge(models.Model):
strecke = models.ForeignKey(Strecke, on_delete=models.CASCADE)
tour = models.ForeignKey("Tour", on_delete=models.CASCADE)
reihenfolge = models.IntegerField()
In my admin, I want to give some restrictions on which sections (Strecke) to show. I thought about using formfield_for_foreignkey. It gets called, but it doesn't have any impact on the options to select from:
#admin.register(Tour)
class TourAdmin(admin.ModelAdmin):
class StreckenreihenfolgeAdminInline(admin.TabularInline):
model = Streckenreihenfolge
autocomplete_fields = ["strecke"]
ordering = ["reihenfolge"]
extra = 0
def formfield_for_foreignkey(self, db_field, request, **kwargs):
print(db_field.name)
if db_field.name == 'strecke':
kwargs['queryset'] = Strecke.objects.filter(auftrag_id__exact=8)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
inlines = [StreckenreihenfolgeAdminInline, ]
Does formfield_for_foreignkey not work for manytomanyfields?
Update
Found some more infos here: Django Admin - Filter ManyToManyField with through model
Apparently formfield_for_manytomany doesn't work for inline forms.
I have then tried to use get_queryset(), which reduced the queryset, but somehow the autocomplete values are still unfiltered.
Maybe this image here is illustrating more what I'm try to achieve:
Finally found the answer here: https://forum.djangoproject.com/t/django-admin-autocomplete-field-search-customization/7455/5
Which results in the following code:
#admin.register(Strecke)
class StreckeAdmin(admin.ModelAdmin):
search_fields = ["name"]
def get_search_results(self, request, queryset, search_term):
print("get serach results")
queryset = queryset.filter(auftrag_id__exact=8)
qs = super().get_search_results(request, queryset, search_term)
print(qs)
return qs

DRF: How to limit fields in nested serializer based on field-permissions?

I'm trying to limit the fields list in serializers based on user permissions. I have a generic routine that does it for all serializers. It's working on the parent serializer, but not on a nested serializer.
I have a client model, and a client profile (referred to as "contacts") as shown below. The client profile model is an extension of the user model (one-to-one relationship).
class Client(AddressPhoneModelMixin, DateFieldsModelMixin, models.Model):
name = models.CharField(max_length=100)
status = models.CharField(max_length=25)
class Meta:
permissions = (
# Object-level
('view_all_clients', 'Can view all clients'),
('change_all_clients', 'Can change all clients'),
# Field-level
('view_client_id', 'Can view client ID'),
('view_client_name', 'Can view client name'),
...others omitted...
)
class ClientProfile(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
blank=True,
null=True,
)
client = models.ForeignKey(
Client,
on_delete=models.PROTECT,
related_name='contacts',
)
receive_invoices = models.BooleanField(default=False)
My object-level permission logic is in the list view:
class ClientList(ListAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ClientSerializer
def get_queryset(self):
user = self.request.user
queryset = None
if user.has_perm('view_client') or user.has_perm('clients.view_all_clients'):
queryset = Client.objects.all().exclude(status__in=['deleted', 'archived'])
if user.has_perm('view_client'): # View only "assigned" clients
if user.type == 'client':
# See if user is a "contact".
queryset = queryset.distinct().filter(contacts__user=self.request.user)
else:
# See if user is assigned to projects for the client(s).
queryset = queryset.distinct().filter(projects__project_users__user=self.request.user)
if queryset is None:
raise PermissionDenied('You do not have permission to view clients.')
return self.get_serializer_class().setup_eager_loading(queryset)
Removing fields from the serializer "fields" property is done in the serializer __init__ method (from examples I found here in SO):
class ClientContactsSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:clientprofile-detail')
user = UserSerializer()
class Meta:
model = ClientProfile
fields = (
'url',
'receive_invoices',
'user',
)
def __init__(self, *args, **kwargs):
super(ClientContactsSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
class ClientSerializer(AddressPhoneSerializerMixin, serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='clients:client-detail')
contacts = ClientContactsSerializer(many=True, read_only=True)
projects = ClientProjectsSerializer(many=True, read_only=True)
class Meta:
model = Client
fields = (
'url',
'id',
'name',
...omitted for brevity...
'contacts',
'projects',
)
def __init__(self, *args, **kwargs):
super(ClientSerializer, self).__init__(*args, **kwargs)
check_field_permissions(self, 'view')
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('country')
return queryset.prefetch_related('contacts', 'contacts__user', 'contacts__user__country', 'projects')
And, finally, here's the check_field_permissions function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
for field_name in fields:
if hasattr(serializer.fields[field_name], 'child'):
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
Stepping through in debug on a page-load of the Client list, I can see that the above function is invoked first for clientprofile, and request is None. The second time, it is invoked for client, and request is a valid request object.
First question, is __init__ the correct place for limiting the list of fields to be serialized?
Second, how to get the request object in the nested serializer (clientprofile)?
After reading a post by Tom Christie, who is an undisputed authority on DRF, I was able to solve my issue. He pointed out that each nested serializer does, in fact, have the context object (and the request, and the user). You just have to deal with nested serializers in the parent __init__ - not their own __init__.
Here's my revised check_field_permissions() function:
def check_field_permissions(serializer, action='view'):
request = serializer.context.get('request', None)
fields = serializer.get_fields()
model = serializer.Meta.model
app_name = model._meta.app_label
model_name = model._meta.model_name
if request is not None and app_name is not None and model_name is not None:
user = request.user
extra_fields = []
for field_name in fields:
if field_name == 'url':
extra_fields.append(field_name)
continue
if hasattr(serializer.fields[field_name], 'child'):
check_field_permissions(serializer.fields[field_name].child, action)
extra_fields.append(field_name)
continue
if not user.has_perm(app_name + '.' + action + '_' + model_name + '_' + field_name):
serializer.fields.pop(field_name)
# If only "url" and child fields remain, remove all fields.
if len(serializer.fields) == len(extra_fields):
for field_name in extra_fields:
serializer.fields.pop(field_name)
It is now recursive. If it hits a field with a "child" attribute, it knows that's a nested serializer field. It calls itself with that child serializer passed as an arg.
The other change is that __init__ was removed from ClientContactsSerializer, because it doesn't need to call check_field_permissions().

Multiple Queries For Inline instances Django admin

I have created a custom Admin for one of my model. And added a Relational table as one of its inline.
1). Now the thing is that Inline has over a 100 rows. And the inline further has a foreign key object to some other model.
2).Whenever i load the model form it takes a whole lot of time to Load.
3).I debugged and checked the code and number of queries. No query was duplicated and the count was nominal.
4).I have overridden the query set for the inline instance to prefetch its foreign key instance.
5).I went on and raised a random validation error in the formset's clean.
6).What i found out that when the error was raised it had a different query for every inline instance. Say if it has a 100 rows then the query was duplicated 100 times for that inline.
7).Is there any way to prefetch or optimize this scenario as its causing way too lag in my application
Here's the Code:
#model
class MyModel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=10)
entity1 = models.ForeignKey("entities.entity1", null=True, blank=True, on_delete=models.CASCADE)
entity2 = models.ForeignKey("entities.entity2", null=True, blank=True, on_delete=models.CASCADE)
is_disabled = models.BooleanField(default=False)
name = models.CharField(max_length=24, blank=True, null=True)
class Meta:
db_table = 'my_model'
def __str__(self):
return "{}:{}".format(self.phone_number, self.entity2)
#admin
class Entity2Admin(admin.GeoModelAdmin, VersionAdmin):
list_filter = ('data_status')
readonly_fields = ('source', 'batch','is_live', )
exclude = ('search_key', 'live_at', 'qc_approved_at')
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for obj in formset.deleted_objects:
obj.delete()
for instance in instances:
if isinstance(instance, MyModel):
if (not instance.created_by):
instance.created_by = request.user
if (not instance.id):
instance.some_field = some_value
instance.save()
formset.save_m2m()
inlines = [MyModelInline]
extra_js = ['js/admin/GoogleMap.js','https://maps.googleapis.com/maps/api/js?key=AIzaSyA-5gVhxxxxxxxxxxxxxxxxxxxxxxx&callback=initGoogleMap']
#MyModelInline
class MyModelInline(admin.TabularInline):
model = MyModel
extra = 0
can_delete = True
show_change_link = False
formset = MyModelFormSet
readonly_fields = ['user']
verbose_name_plural = "MyModels"
fields = ['phone_number', 'name', 'user', 'entity2']
def get_queryset(self, request):
return super(MyModelInline, self).get_queryset(request).select_related('entity2', 'user')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "entity2":
object_id = request.resolver_match.kwargs.get('object_id')
kwargs["queryset"] = Entity2.objects.filter(field=value)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Please help

Django Admin conditional display inlines based on models property

I've got marketplace models with fields:
class VacationEvent(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
process_fba = models.BooleanField(default=True)
#property
def has_amazon_fba_vacation(self):
return hasattr(self, 'amazon_fba_vacation')
And in admin.py:
class FBAInline(admin.StackedInline):
model = AmazonFBAVacation
can_delete = False
verbose_name_plural = 'amazon fba vacation'
fk_name = 'event'
I need to display conditionally FBAInline class when creating\updating VacationEventAdmin. list_filter shows true\false values of "process_fba" field, so if it is true - FBAInline should be displayed:
#admin.register(VacationEvent)
class VacationEventAdmin(admin.ModelAdmin):
inlines = []
list_display = ('id', 'user', 'holiday_name', 'start_date', 'end_date', 'process_fba', 'process_fbm', 'process_walmart', 'process_ebay')
list_filter = ['process_fba', 'process_fbm', 'process_walmart', 'process_ebay', 'status',]
search_fields = ['holiday_name', 'user__username']
# if VacationEvent.process_fba and VacationEvent.has_amazon_fba_vacation:
# inlines.append(FBAInline)
# try:
# getattr(VacationEvent, 'process_fba')
# except AttributeError:
# print "doesn't"
# else:
# inlines.append(FBAInline)
I tried getting attr but don't understant how to compare field values for admin. Thanks for any help.
Override get_formsets_with_inlines method
get_formsets_with_inlines(self, request, obj=None):
for inline in self.get_inline_instances(request, obj):
if obj and obj.has_amazon_fba_vacation:
yield inline.get_formset(request, obj), inline

Django rest framework get or create for PrimaryKeyRelatedField

I start to create REST API for my web-application with Django and Django rest framework and I need one logic problem.
There are entities Instruction and Tag. The user visit my service and create self Instruction and add exists Tag OR new Tag for it.
I created my model seriallizer class with using PrimaryKeyRelatedField for relation Instruction<->Tag. But if I do POST for a new Instruction with new Tag I got error: "Invalid pk \"tagname\" - object does not exist.".
I solved this problem with the overriding of the to_internal_value method in my field class.
What is the best practice for solving this problem? It seems to me this problem is typical for web and REST API.
My models:
class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name
class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title
my serializers:
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('name',)
class InstructionSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
author = serializers.SerializerMethodField()
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author')
read_only_fields = ('modified_datetime',)
I created new field class class PrimaryKeyCreateRelatedField and overrided to_internal_value method for creating the new Tag object instead raising with message 'does_not_exist':
PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
my view:
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
serializer = InstructionSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Update
models.py
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$',
_('Only alphanumeric characters are allowed.'))
class Tag(Model):
name = CharField(max_length=32, verbose_name=_("Name"),
unique=True, validators=[alphanumeric], primary_key=True)
def __str__(self):
return self.name
class Step(PolymorphicModel):
instruction = ForeignKey(Instruction,
verbose_name=_("Instruction"),
related_name='steps',
blank=False, null=False,
on_delete=CASCADE)
position = PositiveSmallIntegerField(verbose_name=_("Position"), default=0)
description = TextField(verbose_name=_("Description"),
max_length=2048,
blank=False, null=False)
class Meta:
verbose_name = _("Step")
verbose_name_plural = _("Steps")
ordering = ('position',)
unique_together = ("instruction", "position")
def __str__(self):
return self.description[:100]
class Instruction(Model):
user = ForeignKey(settings.AUTH_USER_MODEL,
related_name='instructions',
on_delete=CASCADE,
blank=False, null=False,
verbose_name=_("User"))
title = CharField(max_length=256,
verbose_name=_("Title"),
blank=False, null=False)
created_datetime = DateTimeField(verbose_name=_("Creation time"), editable=False)
modified_datetime = DateTimeField(
verbose_name=_("Last modification time"), blank=False, null=False)
tags = ManyToManyField(Tag,
related_name="instructions",
verbose_name=_("Tags"))
# thumbnail = #TODO: image field
class Meta:
ordering = ['-created_datetime']
# singular_name = _("")
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
n = now()
if self.id is None:
self.created_datetime = n
self.modified_datetime = n
super(Instruction, self).save(force_insert, force_update, using, update_fields)
def __str__(self):
return self.title
views.py
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def get_serializer_class(self):
"""Return different serializer class for different action."""
if self.action == 'list':
return InstructionSerializer
elif self.action == 'create':
return InstructionCreateSerializer
serialiers.py
class PrimaryKeyCreateRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
# self.fail('does_not_exist', pk_value=data)
return self.get_queryset().create(pk=data)
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
class InstructionCreateSerializer(serializers.ModelSerializer):
tags = PrimaryKeyCreateRelatedField(many=True, queryset=Tag.objects.all())
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'steps')
read_only_fields = ('modified_datetime',)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction
class InstructionSerializer(serializers.ModelSerializer):
tags = serializers.StringRelatedField(many=True)
author = serializers.SerializerMethodField()
steps = InstructionStepSerializer(many=True)
def get_author(self, obj):
return obj.user.username
class Meta:
model = Instruction
fields = ('id', 'user', 'title', 'created_datetime', 'modified_datetime', 'tags', 'author', 'steps')
read_only_fields = ('modified_datetime',)
In my case to solve the problem I need to override the method run_validation. That allow make check of tags and create their (if not exists) before validation.
class InstructionCreateSerializer(serializers.ModelSerializer):
steps = InstructionStepSerializer(many=True)
user = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Instruction
fields = ('title', 'created_datetime', 'modified_datetime', 'tags', 'steps', 'id', 'user')
read_only_fields = ('modified_datetime',)
def run_validation(self, data=serializers.empty):
if 'tags' in data:
for tag in data['tags']:
Tag.objects.get_or_create(name=tag)
return super(InstructionCreateSerializer, self).run_validation(data)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
steps_data = validated_data.pop('steps')
# NOTE: tags need add after creation of the Instruction object otherwise we will got exception:
# "needs to have a value for field "id" before this many-to-many relationship can be used."
instruction = Instruction.objects.create(**validated_data)
for tag in tags_data:
instruction.tags.add(tag)
for step in steps_data:
Step.objects.create(instruction=instruction,
description=step['description'],
position=step['position'])
return instruction
Apart from the answers given by #YPCrumble and #SijanBhandari, I just had to comment on something in your code.
In the models.py, you have overridden the save method for adding created_at and modified_on. For that you could just add
created_at = models.DateTimeField(auto_now_add=True)
modified_on = DateTimeField (auto_now=True)
The auto_now_add option sets when the object is created for the first time.
It's not editable. The auto_now setting sets whenever the object is saved, ie, whenever object.save() method is called upon.
These usually are used for timestamping the objects for future references.
Why write so many lines, when you could do this on just 2 lines of code.
Just a heads up though!!
For further details, go to the documentation here
In "regular" Django you usually want to create your model instance in the form's save method, not the view. DRF is similar, in that you want to create your model instances in the serializer's create or update methods. The reason for this is that if you need to add a new endpoint to your API you can reuse the serializer and would not have to write duplicate code creating or updating your model instance.
Here's how I'd refactor your code:
Remove the entire create method from your ModelViewSet - you don't need to override that.
Remove the custom PrimaryKeyCreateRelatedField - you just need a PrimaryKeyRelatedField
Add two methods to your serializer - create and update:
In the create method, create your tag objects before saving the instruction object like you can see in the DRF docs. You can get the current user like you were doing in your view via self.context['request'].user in this create method. So you might create the Instruction like Instruction.objects.create(user=self.context['request'].user, **validated_data) and then loop through the tags (like they do for tracks in the docs) to add them to the Instruction.
The docs don't have an example update method but essentially your update method also takes an instance parameter for the existing instruction. See this answer from the creator of DRF for more details
The best way would be sort out everything at your CREATE method of the view.
I believe you tags will be sent from your front-end to the back-end at the format of
[ 1,
{'name': "TEST"},
{'name': 'TEST2'}
]
Here '1' is the existing tag id and 'TEST' and 'TEST2' are the two new tags inserted by
the user. Now you can change your CREATE method as follows:
class InstructionViewSet(viewsets.ModelViewSet):
queryset = Instruction.objects.all()
serializer_class = InstructionSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def create(self, request, *args, **kwargs):
data = dict.copy(request.data)
data['user'] = self.request.user.pk
# MODIFICATION.....
tags = self.request.get('tags', None)
tag_list = []
if tags:
for tag in tags:
if isinstance(tag, dict):
new_tag = Tag.objects.create(name=tag['name'])
tag_list.append(new_tag.id)
else:
tag_list.append(int(tag))
data = {
'title': ....
'tags': tag_list,
'user': ...
'author': ...
......
}
serializer = InstructionSerializer(data=data)
I hope it will be helpful for you.