Custom create method to prevent duplicates - python

I would like to add some logic to my serializer.py.
Currently it creates duplicate tags (giving a new ID to the item, but often it will match a tag name already).
In plain english
if exists:
# Find the PK that matches the "name" field
# "link" the key with Movie Class item
else:
# Create the "name" inside of Tag class
# "link" the key with Movie Class item
The data being posted looks like this:
{
"title": "Test",
"tag": [
{
"name": "a",
"taglevel": 1
}
],
"info": [
]
}
Models.py
class Tag(models.Model):
name = models.CharField("Name", max_length=5000, blank=True)
taglevel = models.IntegerField("Tag level", blank=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField("Whats happening?", max_length=100, blank=True)
tag = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.title
Serializers
class MovieSerializer(serializers.ModelSerializer):
tag = TagSerializer(many=True, read_only=False)
class Meta:
model = Movie
fields = ('title', 'tag', 'info')
def create(self, validated_data):
tags_data = validated_data.pop('tag')
movie = Movie.objects.create(**validated_data)
for tag_data in tags_data:
movie.tag.create(**tag_data)
return movie

This will probably solve your issue:
tag = Tag.objects.get_or_create(**tag_data)[0]
movie.tag.add(tag)
get_or_create function returns tuple (instance, created), so you have to get instance with [0].
So the full code is:
def create(self, validated_data):
tags_data = validated_data.pop('tag')
movie = Movie.objects.create(**validated_data)
for tag_data in tags_data:
tag = Tag.objects.get_or_create(**tag_data)[0]
movie.tag.add(tag)
return movie
To make function case insensitive, you have to do "get or create" manually (the main part is to use __iexact in filter):
tag_qs = Tag.objects.filter(name__iexact=tag_data['name'])
if tag_qs.exists():
tag = tag_qs.first()
else:
tag = Tag.objects.create(**tag_data)
movie.tag.add(tag)

Related

Nested Serializers with different queryset

How to create a nested serializer with it's own queryset?
In the following example I would like to replace an '#api_view' function with a class based view with serializers.
Simplified, I have the following code:
models.py
class Klass(models.Model):
name = models.TextField()
class Pupils(models.Model):
name = models.TextField()
klass = models.ForeignKey(Klass)
class Chapter(models.Model):
"""A Chapter in a school book."""
name = models.TextField()
class TestResult(models.Model):
"""TestResults for a Chapter."""
pupil = models.ForeignKey(Pupil)
chapter = models.ForeignKey(Chapter)
score = models.IntegerField()
view.py
#api_view
def testresults(request, klass_id, chapter_id):
pupils = Pupils.objects.filter(klas__id=klass_id)
tests = Tests.objects.filter(chapter__id=chapter_id)
ret_val = []
for pupil in pupils:
pu = {
"name": pupil.name,
"tests": [{"score": test.score,
"chapter": test.chapter.name} for test in tests.filter(pupil=pupil)]
}
ret_val.append(pu)
return Response({"pupils": ret_val})
url
/api/testresult/<klass_id>/<chapter_id>/

DRF Create new object and link to (but not create) it's related items (nested serializer)?

I'm trying to link multiple items (subdomains) to an item being created (evidence).
My form submits okay - but I'm trying to figure out the 'best' way to go about this.
According to the docs I have to override the create method - but their example shows them 'creating' the related objects.
I don't want to do that. I want to just just add those related items to the piece of evidence I am creating (to create the relationship)
Here are my serializers:
class SubdomainSerializer(serializers.ModelSerializer):
class Meta:
model = Subdomain
fields = [
"id",
"domain",
"short_description",
"long_description",
"character_code",
]
class EvidenceSerializer(serializers.ModelSerializer):
"""
"""
created_by = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
updated_by = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
absolute_url = serializers.SerializerMethodField()
created_by_name = serializers.SerializerMethodField()
updated_by_name = serializers.SerializerMethodField()
subdomains = SubdomainSerializer(many=True)
class Meta:
model = Evidence
fields = "__all__"
extra_kwargs = {
"subdomains": {
"error_messages": {
"null": "This field is required.",
"blank": "This field is required.",
}
},
}
def get_absolute_url(self, obj):
return obj.get_absolute_url()
def get_created_by_name(self, obj):
return obj.created_by.full_name
def get_updated_by_name(self, obj):
return obj.updated_by.full_name
def create(self, validated_data):
subdomains_data = validated_data.pop("subdomains")
evidence = Evidence.objects.create(**validated_data)
for subdomain in subdomains_data:
# This is where I want to add the subdomains to the evidence - not create ones - how best to accomplish that?
Subdomain.objects.create(**subdomain)
return evidence
def update(self, instance, validated_data):
# Add the requestor as the updater in a PATCH request
request = self.context["request"]
validated_data["updated_by"] = request.user
return super().update(instance, validated_data)
Is there a better way to setup my serializers? Should I be doing something different to make this more achievable?
Edited to add my models:
class Subdomain(CreateUpdateMixin):
"""
"""
domain = models.ForeignKey(Domain, on_delete=models.PROTECT)
short_description = models.CharField(max_length=100)
long_description = models.CharField(max_length=250)
character_code = models.CharField(max_length=5)
proficiency_levels = models.ManyToManyField(SubdomainProficiencyLevel)
class Meta:
verbose_name = "Subdomain"
verbose_name_plural = "Subdomains"
def __str__(self):
"""Unicode representation of Subdomain."""
return f"{self.character_code}"
class Evidence(CreateUpdateMixin, CreateUpdateUserMixin, SoftDeletionModel):
"""
"""
subdomains = models.ManyToManyField(Subdomain, related_name="evidences")
evaluation = models.ForeignKey(
Evaluation, related_name="evidences", on_delete=models.PROTECT
)
published = models.BooleanField(default=False)
comments = models.CharField(max_length=500)
class Meta:
ordering = ["-created_at"]
verbose_name = "Evidence"
verbose_name_plural = "Evidence"
def __str__(self):
"""Unicode representation of Evidence."""
return f"{self.subdomain} : {self.comments}"
def get_absolute_url(self):
"""Return absolute url for Evidence."""
return reverse("evidence-detail", args=[str(self.id)])
Here you want to set the subdomain ForeignKey to the new created evidence.
For that, you just have to retrieve the subdomain and update his evidence field like this :
def create(self, validated_data):
subdomains_data = validated_data.pop("subdomains")
evidence = Evidence.objects.create(**validated_data)
for subdomain in subdomains_data:
# Retrieve the subdomain and update the evidence attribute
sub_id = subdomain['id']
sub = Subdomain.objects.get(id=sub_id)
sub.evidence = evidence
sub.save()
return evidence
NB : I use the default inverse ForeignKey relationship : sub.evidence

The serializer field might be named incorrectly and not match any attribute or key on the `QuerySet` instance

I am using Django Rest and my request parameter contains:
[
{
"job_role": 2,
"technology": 1
},
{
"job_role": 1,
"technology": 1
},
{
"job_role": 2,
"technology": 1
}
]
My models are:
class Technology(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class JobRole(models.Model):
role_name = models.CharField(max_length=100)
def __str__(self):
return self.role_name
class ExpertPricing(models.Model):
role_name = models.OneToOneField(JobRole, related_name="role", on_delete=models.SET_NULL, null=True)
experience_in_years = models.PositiveBigIntegerField()
technology = models.OneToOneField(Technology, related_name="technology", on_delete=models.SET_NULL, null=True)
salary_per_month = models.PositiveBigIntegerField()
My view looks like this:
class PricingView(APIView):
def post(self, request):
datas = request.data
data_list = []
for data in datas:
job_role_id = data["job_role"]
technology_id = data["technology"]
job_role = JobRole.objects.get(pk=job_role_id)
technology = Technology.objects.get(pk=technology_id)
expert_pricing = ExpertPricing.objects.filter(role_name=job_role, technology=technology)
if expert_pricing:
data_list.append(expert_pricing)
serializer = ExpertPricingSerializer(data_list, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
serializers.py
class TechnologySerializer(serializers.ModelSerializer):
class Meta:
model = Technology
fields = ("id", "name")
class JobRoleSerializer(serializers.ModelSerializer):
class Meta:
model = JobRole
fields = ("id","role_name")
class ExpertPricingSerializer(serializers.ModelSerializer):
role = JobRoleSerializer(many=False, read_only=True)
technology = TechnologySerializer(many=False, read_only=True)
class Meta:
model = ExpertPricing
fields = "__all__"
I am unable to understand why data_list is not being serialized.
the error says:
AttributeError: Got AttributeError when attempting to get a value for field `experience_in_years` on serializer `ExpertPricingSerializer`.
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 'experience_in_years'.
Since you defined in your model that those two fields (experience_in_years and salary_per_month) can not be empty, it seems like you need to do one of these things:
Send experience_in_years and salary_per_month fields in your request too.
Give a default value to those fields
Make it null=True, blank=True
If you do 2 or 3 those solutions require migration, keep that in mind, after doing one of those things you should be good to go

Original exception text was: 'RelatedManager' object has no attribute 'type'. - Django Rest

models.py
class Sequence(models.Model):
category_id = models.ForeignKey(SequenceCategory, on_delete=models.CASCADE)
description = models.TextField()
code = models.CharField(max_length=50)
total_divisions = models.IntegerField()
status = models.BooleanField(default=True)
def __str__(self):
return self.code
class SequenceValue(models.Model):
TYPE_CHOICES =(
('N', 'Numeric'),
('A', 'Alphabet')
)
sequence_id = models.ForeignKey(Sequence, on_delete=models.CASCADE, blank=True, null=True, related_name='Sequence_details')
type = models.CharField(max_length=100, choices=TYPE_CHOICES)
value = models.CharField(max_length=50)
starting_value = models.CharField(max_length=50, null=True, blank=True)
increment_value = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return self.value
serializers.py
class SequenceValueSerializer(serializers.ModelSerializer):
class Meta:
model = SequenceValue
# fields = '__all__'
exclude = ['sequence_id']
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer()
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
SequenceValue.objects.create(sequence_id=sequence_id, **Sequence_details_data)
return sequence_id
views.py
class SequenceCreate(ListCreateAPIView):
queryset = Sequence.objects.all()
serializer_class = SequenceSerializer
Why do I get the error? When I refer to n number of articles I got a solution that put many=True something like this.
Sequence_details = SequenceValueSerializer(many=True)
But when I make changes on that then I can't get the Sequence details fields. How can I fix this issue? Can you give a solution, please?
Actual error-
Got AttributeError when attempting to get a value for field type on serializer SequenceValueSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the RelatedManager instance.
Original exception text was: 'RelatedManager' object has no attribute 'type'.
Data Passess
Data - {
"Sequence_details": [{
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
}],
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
Actual eroor screenshot
Adding many=True works because your request data passes Sequence_details as a list. In order to use it in your serializer, you can just iterate through the field and create your objects like this:
class SequenceSerializer(serializers.ModelSerializer):
Sequence_details = SequenceValueSerializer(many=True)
class Meta:
model = Sequence
fields = '__all__'
def create(self, validate_data):
Sequence_details_data = validate_data.pop('Sequence_details')
sequence_id = Sequence.objects.create(**validate_data)
for details in Sequence_details_data
SequenceValue.objects.create(sequence_id=sequence_id, **details)
return sequence_id
But if you don't want to do this, you can remove many=True and keep your serializer as is, but ensure that Sequence_details in your request data is not a list:
{
"Sequence_details": {
"type": "N",
"value": "123",
"starting_value": "1",
"increment_value": "1"
},
"description": "asd",
"code": "qwe",
"total_divisions": 2,
"status": false,
"category_id": 3
}
I can't comment so I will just mention it here :). type is actually a function in python which tells you about the class type of argument. I am not sure if this the issue but the can you try by renaming type to _type or something of your choice.

GraphQL Mutation in Graphene for Object with Foreign Key Relation

I'm building a simple CRUD interface with Python, GraphQL (graphene-django) and Django. The CREATE mutation for an Object (Ingredient) that includes Foreign Key relations to another Object (Category) won't work. I want to give GraphQL the id of the CategoryObject and not a whole category instance. Then in the backend it should draw the relation to the Category object.
In the Django model the Ingredient Object contains an instance of the Foreign key Category Object (see code below). Is the whole Category Object needed here to draw the relation and to use Ingredient.objects.select_related('category').all()?
The create mutation expects IngredientInput that includes all properties and an integer field for the foreign key relation. So the graphQL mutation itself currently works as I want it to.
My question is similar if not the same as this one but these answers don't help me.
models.py:
class Category(models.Model):
name = models.CharField(max_length=50, unique=True)
notes = models.TextField()
class Meta:
verbose_name = u"Category"
verbose_name_plural = u"Categories"
ordering = ("id",)
def __str__(self):
return self.name
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Meta:
verbose_name = u"Ingredient"
verbose_name_plural = u"Ingredients"
ordering = ("id",)
def __str__(self):
return self.name
schema.py:
class CategoryType(DjangoObjectType):
class Meta:
model = Category
class CategoryInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
class IngredientType(DjangoObjectType):
class Meta:
model = Ingredient
class IngredientInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
category = graphene.Int()
class CreateIngredient(graphene.Mutation):
class Arguments:
ingredientData = IngredientInput(required=True)
ingredient = graphene.Field(IngredientType)
#staticmethod
def mutate(root, info, ingredientData):
_ingredient = Ingredient.objects.create(**ingredientData)
return CreateIngredient(ingredient=_ingredient)
class Mutation(graphene.ObjectType):
create_category = CreateCategory.Field()
create_ingredient = CreateIngredient.Field()
graphql_query:
mutation createIngredient($ingredientData: IngredientInput!) {
createIngredient(ingredientData: $ingredientData) {
ingredient {
id
name
notes
category{name}
}
graphql-variables:
{
"ingredientData": {
"name": "milk",
"notes": "from cow",
"category": 8 # here I ant to insert the id of an existing category object
}
}
error-message after executoin the query:
{
"errors": [
{
"message": "Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance.",
"locations": [
{
"line": 38,
"column": 3
}
],
"path": [
"createIngredient"
]
}
],
"data": {
"createIngredient": null
}
}
I had this same problem today.
The Cannot assign \"8\": \"Ingredient.category\" must be a \"Category\" instance. error is a Django error that happens when you try to create an object using the foreign key integer directly instead of an object.
If you want to use the foreign key id directly you have to use the _id suffix.
For example, instead of using:
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=8)
You have to use either
category_obj = Category.objects.get(id=8)
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category=category_obj)
or
_ingredient = Ingredient.objects.create(name="milk", notes="from_cow", category_id=8)
In the case of using GraphQL, you would have to set your InputObjectType field to <name>_id. In your case:
class IngredientInput(graphene.InputObjectType):
name = graphene.String(required=True)
notes = graphene.String()
category_id = graphene.Int()
This, however will make your field in the schema show up as categoryId. If you wish to keep the category name, you must change to:
category_id = graphene.Int(name="category")
Cheers!

Categories