How to loop through a jsonfield serializer - python

I've been trying to loop through the incoming data from a json serializer for a couple of weeks now. Tried a few different approaches, but i can't seem to loop through the json and return a list with all facility_id's for example. Ultimately i want to create one leadfacility object for each json item using it's facility_id and it's datetime. But I can't even seem to access the facility_id when using a for loop.
The facilities that are being assigned are already inside the database.
Does anyone know what I'm missing here? How can i loop though "assigned_facilities"? The only thing I am able to return is the entire json data all at once with print(). Or is my json data structured in the wrong way?
class LeadUpdateSerializer(serializers.ModelSerializer):
assigned_facilities = serializers.JSONField(required=False, allow_null=True, write_only=True)
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
instance = Lead.objects.create(**validated_data)
for item in assigned_facilities:
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get('datetime'))
return instance
json
{
"assigned_facilities": [{
"facility_id": "1",
"datetime": "2018-12-19 09:26:03.478039"
},
{
"facility_id": "1",
"datetime": "2019-12-19 08:26:03.478039"
}
]
}
models.py
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(null=True, blank=True)
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}"

I'd suggest a different, arguably better approach. Instead of JSONField use a nested model serializer.
class AssignedFacilitySerializer(serializers.ModelSerializer):
fields = ('assigned_facilities_id', 'datetime')
model = Facility
class LeadUpdateSerializer(serializers.ModelSerializer):
assigned_facilities = AssignedFacilitySerializer(many=True)
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
instance = Lead.objects.create(**validated_data)
for item in assigned_facilities:
instance.leadfacility.create(**item)
return instance
You haven't posted your Facility model, so there may be some improperly named fields, maybe the related_name too (leadfacility).

you are doing :
for item in assigned_facilities:
instance.leadfacility.create(assigned_facilities_id=assigned_facilities.get('facility_id'), datetime=assigned_facilities.get('datetime'))
but not using item anywhere.

for item in assigned_facilities:
instance.leadfacility.create(assigned_facilities_id=item['facility_id'], datetime=item['datetime'])

I see a few issues in both your models and how you add facilities to a lead in your loop.
Please take a look at https://docs.djangoproject.com/en/4.1/topics/db/models/#extra-fields-on-many-to-many-relationships and edit your models accordingly.
TL;DR
You can then put extra fields on the intermediate model.
This is LeadFacilityAssign in your case.
The intermediate model is associated with the ManyToManyField using the through argument to point to the model that will act as an intermediary.
You are missing this part. Make sure to add a ManyToManyField to Leads.
If you keep reading the docs, you will also find that you can use add(), create(), or set() to create relationships between Lead and Facility using the through model.
You mentioned facilities already exist, so create() is the wrong method.
Please use add() or set() according to your use case and pre-fetch those facilities using their id.

When you use pop() it gets and then deletes the data. That is why u are unable to process it in for loop.
Use get() instead of pop(), and ur problem will be solved.
Change this line:
assigned_facilities = validated_data.pop("assigned_facilities")
to:
assigned_facilities = validated_data.get("assigned_facilities")

Related

Calling a model's method inside a serializer and handling a received object

I'm supposed to write an API for the endpoints. It should be an application inside an existing project. I should work with its models and i'm not allowed to alter them in any way.
The project consists of multiple applications, and some applications have their own models.
There is an exempt from CategoryMetall/models.py in the CatalogNew application:
class CategoryMetall(MPTTModel):
position = models.ForeignKey(
Menu,
on_delete=models.CASCADE,
verbose_name="foo",
blank=True,
null=True,
)
subPosition = TreeForeignKey(
"self",
on_delete=models.CASCADE,
verbose_name="bar",
blank=True,
null=True,
)
def parent(self):
if self.subPosition:
return self.subPosition
else:
return self.position
As i understood, the parent() method is supposed to return an object of either a CategoryMetall model, or a Menu model. A Menu model is a model of another application from the project.
Here is an exempt from it as well:
Menu/models.py
class Menu(models.Model):
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
verbose_name="parent category",
null=True,
blank=True,
)
So, i figured that in order to get a parent category i'm supposed to use the CategoryMetall.parent() method written by some other developer.
The issue is, i'm also supposed to somehow serialize it.
I have written a serializer in my serializers.py:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
parentCategory = serializers.ReadOnlyField(source='parent')
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'parentCategory']
And a view for it, views.py:
class CategoryMetallViewSet(viewsets.ModelViewSet):
queryset = CategoryMetall.objects.all()
serializer_class = CategoryMetallSerializer
pagination_class = CustomPagination
I have registered a url for this view in my urls.py as well:
router.register(r'catmet', views.CategoryMetallViewSet)
urlpatterns = [
path('myapi/', include(router.urls)),
]
The thing is, when i go to myapi/catmet link to see how it looks, i get an exception:
TypeError: Object of type Menu is not JSON serializable
As i understood, when i use
serializers.ReadOnlyField(source='parent')
it calls the parent() method of a CategoryMetall model, then returns by a foreign key an object of Menu model. It goes straight into a serializer and breaks because its somehow not serializable.
An object itself, as i got it from the debug screen, looks like this:
<Menu: Metallurgy raw materials >
I'm not sure if i'm using the right approach to call the method from the serializer, and even if i do, i have no idea what to do to serialize that.
I tried to search the Django Rest Framework documentation, google, reddit and StackOverflow to find out how to do it properly, or what exactly i do wrong, but failed. I'm still an intern, so i dont have an extensive knowledge of the framework and only started working with it like a week ago.
I investigated on how to serialize the foreign key itself and found out that its done by writing another serializer specifically for the model a foreign key refers to, then using it inside the main one. But i don't know how to do that in this case, or if it even is a solution.
Can you please suggest something?
As i understood, when i use `serializers.ReadOnlyField(source='parent')` it calls the parent() method of a CategoryMetall model, then returns by a foreign key an object of Menu model
That's correct.
The problem with the parent method is that it returns one of two model types: Menu or CategoryMetall (self).
I personally see only the option to return both objects in the API call and then check later in the app or whatever this is used if the subPosition is available or not.
With this approach you can define a new serializer for the Menu. Django doesn't know how to return <Menu: Metallurgy raw materials > as JSON. You have to tell it which fields it should serialize. Exactly like in the CategoryMetallSerializer. For example:
class MenuSerializer(serializers.ModelSerializer):
class Meta:
model = Menu
fields = ['field_1', 'field_2'] # all fields you want to fetch from the menu
Now you can use this serializer inside the CategoryMetallSerializer:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
position = MenuSerializer(read_only=True)
subPosition = CategoryMetallSerializer(read_only=True)
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'position', 'subPosition']
I've typed this out of my head. There might be some syntax issues in the code as it is not tested but I hope I could point you in the right direction. BTW +1 for the details in your question.
EDIT 1 (comment 1: only serialize one field)
If you want to change the output of the serializer, you can override the to_representation function of the serializer like that:
class CategoryMetallSerializer(serializers.HyperlinkedModelSerializer):
position = MenuSerializer(read_only=True)
subPosition = CategoryMetallSerializer(read_only=True)
class Meta:
model = CategoryMetall
fields = ['id', 'name', 'position', 'subPosition']
def to_representation(self, instance):
data = super().to_representation(instance)
print(data) # for debug reasons
# modify data as you wish - I'm actually not sure if this is a dict and if the following works
if data.get('subPosition'):
del data['position']
else:
del data['subPosition']
return data
Another approach would be something in this way from the official docs:
https://www.django-rest-framework.org/api-guide/relations/
def to_representation(self, value):
"""
Serialize bookmark instances using a bookmark serializer,
and note instances using a note serializer.
"""
if isinstance(value, Bookmark):
serializer = BookmarkSerializer(value)
elif isinstance(value, Note):
serializer = NoteSerializer(value)
else:
raise Exception('Unexpected type of tagged object')
return serializer.data

How to filter a one-to-one generic relation with Django?

I have a moderation model :
class ItemModeration(models.Model):
class Meta:
indexes = [
models.Index(fields=['object_id', 'content_type']),
]
unique_together = ('content_type', 'object_id')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
published = models.BooleanField(default=False)
...
A descriptor to attach a moderation object on-the-fly :
class ItemModerationDescriptor(object):
def __init__(self, **default_kwargs):
self.default_kwargs = default_kwargs
def __get__(self, instance, owner):
ctype = ContentType.objects.get_for_model(instance.__class__)
try:
moderation = ItemModeration.objects.get(content_type__pk=ctype.id,
object_id=instance.pk)
except ItemModeration.DoesNotExist:
moderation = ItemModeration(item=instance,**self.default_kwargs)
moderation.save()
return moderation
And a model I want to moderate :
class Product(models.Model):
user = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL)
created = models.DateTimeField(
auto_now_add=True,
blank=True, null=True,
)
modified = models.DateTimeField(
auto_now=True,
blank=True, null=True,
)
name = models.CharField(
max_length=PRODUCT_NAME_MAX_LENGTH,
blank=True, null=True,
)
moderation = ItemModerationDescriptor()
Now I can see a product 'published' state easily :
p=Product(name='my super product')
p.save()
print(p.moderation.published)
-> False
The generic relation is useful because I will be able to search the objects to moderate whatever the type is : it could be products, images, comments.
to_moderate_qs = ItemModeration.objects.filter(published=False)
Now, how can I get a filtered list of published products ?
I would like to do something like this
published_products_qs = Product.objects.filter(moderation__published=True, name__icontains='sony')
But, of course, it won't work as moderation attribute is not a Django model field.
How can I do that efficiently ? I am thinking a about an appropriate JOIN, but I cannot see how to do that with django without using raw SQL.
Django has a great built in answer for this: the GenericRelation. Instead of your descriptor, just define a generic relation on your Product model and use it as a normal related field:
from django.contrib.contenttypes.fields import GenericRelation
class Product(models.Model):
...
moderation = GenericRelation(ItemModeration)
Then handle creation as you normally would with a related model, and filtering should work exactly as you stipulated. To work as your current system, you'd have to put in a hook or save method to create the related ItemModeration object when creating a new Product, but that's no different from other related django models. If you really want to keep the descriptor class, you can obviously make use of a secondary model field for the GenericRelation.
You can also add related_query_name to allow filtering the ItemModeration objects based only on the Product content type.
WARNING if you do use a GenericRelation note that it has a fixed cascading delete behavior. So if you don't want ItemModeration object to be deleted when you delete the Product, be careful to add a pre_delete hook or equivalent!
Update
I unintentionally ignored the OneToOne aspect of the question because the GenericForeignKey is a one-to-many relation, but similar functionality can be effected via smart use of QuerySets. It's true, you don't have access to product.moderation as a single object. But, for example, the following query iterates over a filtered list of products and extracts their name, the user's username, and the published date of the related ModerationItem:
Product.objects.filter(...).values_list(
'name', 'user__username', 'moderation__published'
)
You'll have to use the content_type to query the table by specific model type.
like this:
product_type = ContentType.objects.get_for_model(Product)
unpublished_products = ItemModeration.objects.filter(content_type__pk=product_type.id, published=False)
For more details on the topic check contenttypes doc

How to obtain an object from a queryset of another object list in django

i am developing a project with django and i have a little problem.
I am trying to get an array from a querySet of another object. Trying to obtain the "articulo" who has the "carro_det.id_articulo_fk" field, and after send it to the context of my template:
But in the querySet i am getting the error 'int() argument must be a string, a bytes-like object or a number, not 'Articulo''
Specifically in the line: articulo[i]=Articulo.objects.get(pk=carro_det[i].id_articulo_fk)
This is from my views.py:
def index(request, id_user):
carro=Carro.objects.get(id_usuario_fk=id_user)
carro_det=Carro_det.objects.filter(id_articulo_fk=carro.id)
#HERE IS THE PROBLEM
for i in range(len(carro_det)):
articulo[i]=Articulo.objects.get(pk=carro_det[i].id_articulo_fk)
contexto = {'articulo':articulo,
'carro_det':carro_det}
return render(request, 'cart/cart.html', contexto)
And this is from my models.py, as you can see everything is fine here:
class Carro(models.Model):
total = models.FloatField(default=0, null=True, blank=True)
#llaves foraneas
id_usuario_fk=models.ForeignKey('myuser.Usuario', null=True, blank=True, on_delete=models.CASCADE, db_column='id_usuario_fk')
def __str__(self):
return "id_carro_cliente: " + str(self.id)
class Carro_det(models.Model):
cantidad = models.IntegerField(default=0)
precio_venta = models.FloatField(default=0)
total = models.FloatField(default=0)
#llaves foraneas
id_carro_fk=models.ForeignKey('Carro', null=True, blank=True, on_delete=models.CASCADE, db_column='id_carro_fk')
id_articulo_fk=models.ForeignKey('article.Articulo', on_delete=models.CASCADE, db_column='id_articulo_fk')
def __str__(self):
return "numero de carro asociado: " + str(self.id_carro_fk.pk)
I hope anyone can help me with this, Thank you!.
these 2 attributes:
id_carro_fk=models.ForeignKey('Carro', null=True, blank=True, on_delete=models.CASCADE, db_column='id_carro_fk')
id_articulo_fk=models.ForeignKey('article.Articulo', on_delete=models.CASCADE, db_column='id_articulo_fk')
are objects not PK's, even though that is how you labeled them. You could do:
articulo[i]=Articulo.objects.get(pk=carro_det[i].id_articulo_fk.pk) # notice the .pk at the end
But that isn't the real problem here. It would seem you need to more carefully read the django docs on relationships. It seems like you are accessing all the related Carro_det objects to the Carro instance by making those 2 queries, when you could just access the related attribute.
When declaring a ForeignKey field in django you are accessing the related object directly, with django creating the id field under the covers. This relationship can be accessed on the other (many) side by accessing:
RelatedModel.FKmodel_set
or if specified like so:
class Carro(Model):
field = ForeignKey('Model', related_name='related_fields', ...)
then:
# Carro_det instance
instance.related_fields # this accesses all carros related to this carro_det
# but this is a queryset you can filter down further, it is fetched lazily
or in your case:
carro_instance.carro_det_set
Instead of the way you are doing it...
carro_det[i].id_articulo_fk is returning the foreignkey(Articulo) instance. Instead, you need to use id_articulo_fk_id which is the actual pk of the Articulo instance.
Use:
articulo[i]=Articulo.objects.get(pk=carro_det[i].id_articulo_fk_id)

How to limit the queryset of related fields of serializer based on some request parameters in Django Rest Framework

I have three models Transaction, Business, and Location. They are defined as follows:
class Business(models.Model):
# Can have zero or more locations. A user can have many businesses.
name = models.CharField(max_length=200, validators=[MinLengthValidator(1)])
# ... and some other fields ...
class Location(models.Model):
# Must have one business. Locations cannot exist without a business
suburb = models.CharField(max_length=150, validators=[MinLengthValidator(1)])
business = models.ForeignKey(Business, related_name='locations')
# ... and some other fields ...
class Transaction(models.Model):
# Can have zero or one business
# Can have zero or one location and the location must belong to the business. If business is empty, location must be empty
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
# ... and some other fields ...
And the serializers:
class BusinessRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
owner = get_owner_from_context(self.context)
return Business.objects.filter(owner=owner)
class LocationRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
params = self.context['request'].query_params
business_params = params.get('business')
if business_params is not None:
owner = get_owner_from_context(self.context)
return Location.objects.filter(owner=owner, business=business_params)
else:
return None
class TransactionSerializer(serializers.ModelSerializer):
business = BusinessRelatedField(required=False, allow_null=True)
location = LocationRelatedField(required=False, allow_null=True)
The problem I was facing was that I didn't know how to restrict the value of Location based on the value of Business. I was manually performing this check inside TransactionSerializer's validate method until it occurred to me to create a PrimaryKeyRelatedField subclass and override the get_queryset method. This seemed like a better approach to me (and it's actually working) but I'd like to know if this is the 'normal' way of doing it.
The other problem I'm now facing is that the 'browsable API' no longer shows any choices for Location which I feel is a hint that I might be doing something wrong.
Any help would be appreciated.
You can override the get_fields() method of the serializer and modify the queryset for business and location fields to the desired values.
get_fields() method is used by the serializer to generate the field names -> field instances mapping when .fields property is accessed on it.
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transaction
fields = (.., business, transaction)
def get_fields(self):
# get the original field names to field instances mapping
fields = super(TransactionSerializer, self).get_fields()
# get the required parameters
owner = get_owner_from_context(self.context)
business_params = self.context['request'].query_params.get('business')
# modify the queryset
fields['business'].queryset = Business.objects.filter(owner=owner)
fields['location'].queryset = Location.objects.filter(owner=owner, business=business_params)
# return the modified fields mapping
return fields
This is a very late answer, however it would not be different back then.
With the information you provided (in the comments as well) and AFAIK there is no way of doing this unless you manipulate the javascript code of the browsable API's templates and add ajax calling methods to it.
DRF browsable API and DRF HTML and forms may help.

django - having model class how to get list of all fields, their types and parameters passed?

lets assume I have following model:
class Note(models.Model):
user = models.ForeignKey(User)
pub_date = models.DateTimeField()
title = models.CharField(max_length=200)
body = models.TextField()
def __unicode__(self):
return self.title
I need a function that will work like this:
print inspectModelClass(Note)
>>> {user:('ForeignKey', {null:False, unique:False, blank:False...}), pub_date:('DateTimeField',{null:False, unique:False,...})...}
I don't know how to list only instances of django.model.field, how to get their names, proper types (BooleanField, CharField, etc.) and their properties like null, unique, max_chars etc.
Can you help me with that?
The reason I need this is that having such a function I would be able to dynamically create Index classes for django-haystack.
You can get Model fields properties easily using the class Metadata.
MyModel._meta.fields
return a list of the fields.
Every field in this list has your well known attributes (name, verbose_name etc.)

Categories

Resources