django nested serialization of many to many relationship having an intermediate table - django

I have seen a lot of posts on how to deserialize and serialize nested relationships of tables having a many to many relationship, but when an intermediate table is used in a many to many relationship I am not able to achieve deserialization.
This is because an intermediate table requires two foreign keys, one each from the two tables participating in the relation.
I have an Order model and a Service model which are in a many to many relationship via an OrderItem intermediate table.
I need to pass a JSON request like this:
{"service_time":"2015-11-14 10:00:51+0530",
"address":"xyz",
"items":[{"order": "1", "service_id":"4"},
{"order":"1", "service_id":"5"}]
}
The "service_time" and "address" elements get saved in the Order table. Now the problem arises with the "items" JSON array. I pass the "service_id" (foreign key to the Service table) and I need to pass "order" (foreign key to the Order table) too as it is a required field. The problem is that the primary key of the Order table is not known when the request is sent(as the Order is also created as a part of the same request). How can I achieve deserialization in this scenario?
I tried something like this but it didn't work out.
class OrderSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True)
class Meta:
model = Order
def create(self, validated_data):
items_data = validated_data.pop('items')
orders = Order.objects.create(**validated_data)
for item in items_data:
#order = item['order']
service = item['service']
//Passing the Order object just created as the foreign key of OrderItem
orderItem = OrderItem.objects.create(order=orders, service=service)
orderItem.save()
return orders
class ServiceSerializer(serializers.ModelSerializer):
group = serializers.CharField(source="group.group_name")
category = serializers.IntegerField(source="group.category_id")
class Meta:
model = Service
fields = ['id', 'service_name', 'price', 'special_price', 'service_time', 'group', 'category']
class ItemSerializer(serializers.ModelSerializer):
service_detail = ServiceSerializer(source="service", read_only=True)
class Meta:
model = OrderItem
I get an error saying 'Service' object has no attribute 'order'.
I know the Service model does not have an "order" attribute, but I am creating an OrderItem object, not a Service object.
Any suggestions will be helpful!
Edit: Adding the models used
class Order(models.Model):
STATUSES = [('PENDING', 'Pending'), ('PROCESSING', 'Processing'), ('COMPLETE', 'Complete'), ('CANCELED', 'Canceled')]
PAYMENT_STATUSES = [('PENDING', 'Pending'), ('PAID', 'Paid'),]
CANCEL_REASON = [('NO_SERVICE', 'No Service In Area'), ('NO_STYLIST', 'Stylist Not Available'), ('STYLIST_REFUSED', 'Stylist Refused After Accepting',),
('CUSTOMER_UNREACHABLE', 'Customer Not Reachable'), ('CUSTOMER_CANCELED', 'Customer Canceled at Fisrt Call'), ('CUSTOMER_REFUSED', 'Customer Refused at Last Moment'),
('DUPLICATE_ORDER', 'Duplicate Order'), ('MALE_CLIENT', 'Male Client'), ('CALLCENTER_DELAY', 'Delay/Error at Frontdesk')]
serial = models.CharField(max_length=10, null=True, blank=True,)
customer = models.ForeignKey(Customer, verbose_name="customer", related_name="ordersbycustomer")
stylist = models.ForeignKey(Stylist, null=True, blank=True, verbose_name="stylist", related_name="ordersbystylist")
# TODO, Use timezone.now
service_time = models.DateTimeField(default=timezone.now, blank=True)
started_moving = models.DateTimeField(null=True, blank=True)
service_start_at = models.DateTimeField(null=True, blank=True)
service_end_at = models.DateTimeField(null=True, blank=True)
reached_safely = models.DateTimeField(null=True, blank=True)
sub_total = models.FloatField(default=0)
discounts = models.FloatField(default=0)
grand_total = models.FloatField(default=0)
total_time = models.IntegerField(default=0)
status = models.CharField(max_length=32, choices=STATUSES, default='PENDING')
payment_status = models.CharField(max_length=32, choices=PAYMENT_STATUSES, default='PENDING')
items = models.ManyToManyField(Service, through='OrderItem')
address = models.ForeignKey(Address, null=True, blank=True, related_name='+', on_delete=models.SET_NULL)
amount_received = models.FloatField(default=0)
send_sms = models.BooleanField(default=True)
thru_app = models.BooleanField(default=True)
referral_discount = models.FloatField(default=0)
cancellation_reason = models.CharField(max_length=64, choices=CANCEL_REASON, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return self.serial
def _get_service_list(self):
return ','.join(str(p.description) for p in self.items.all())
service_list = property(_get_service_list)
class Service(models.Model):
group = models.ForeignKey(Group, related_name="services")
service_name = models.CharField(max_length=128)
price = models.FloatField(default=0)
special_price = models.FloatField(default=0)
service_time = models.IntegerField()
description = models.CharField(max_length=123)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return '{} ({})'.format(self.service_name, self.group)
class OrderItem(models.Model):
order = models.ForeignKey(Order)
service = models.ForeignKey(Service)
price = models.FloatField(default=0)
special_price = models.FloatField(default=0)
qty = models.IntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __unicode__(self):
return self.service.service_name
Edit2: Added the other related serializers.
One important thing I forgot to mention is that, the data gets saved in the DB, but the exception is still raised.

Related

django objects.create method is too slow How to make faster?

multiple tables are mapped and, when I create post request,
it takes about 2~3 seconds. Is there any ways to fix it?
I guess it takes a long time on:
objects.create
for loop
product.objects.get
however, I am not able to find the better ways..
models:
#product, Order, OrderItems, ShippingAddress are mapped
class Order(models.Model):
user = models.ForeignKey(User, on_delete= models.CASCADE)
order_date = models.DateTimeField(auto_now=True)
is_paid = models.BooleanField(default=False)
paid_at = models.DateTimeField(auto_now=False, null=True, blank=True)
delivery_code = models.CharField(max_length=255, null=True, blank=True)
is_delivered = models.BooleanField(default=False)
delivered_date = models.DateTimeField(auto_now=False, null=True, blank=True)
total_price = models.DecimalField(max_digits=7, decimal_places=2, null=True)
shipping_price = models.DecimalField(max_digits=7, decimal_places=2, null=True)
payment_method = models.CharField(max_length=255,null=True)
def __str__(self):
return str(self.user)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete= models.CASCADE, null=True, blank=True)
product = models.ForeignKey(Product, on_delete= models.CASCADE)
name = models.CharField(max_length=200, null=True)
image = models.CharField(max_length=255, null=True)
qty = models.IntegerField(default=0, null=True)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True)
def image_preview(self):
if self.image:
return mark_safe('<img src="{0}" width="55" height="55" />'.format(self.image))
else:
return '(No image)'
def __str__(self):
return str(self.product)
class ShippingAddress(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
order = models.OneToOneField(Order, on_delete=models.CASCADE, null=True, blank=True)
address = models.CharField(max_length=255, null=False)
city = models.CharField(max_length=255, null=False)
postal_code = models.CharField(max_length=255, null=False)
country = models.CharField(max_length=255, null=False)
def __str__(self):
return str(self.user)
view:
#permission_classes(IsAuthenticated)
#api_view(['POST'])
def OrderCreate(request):
data = request.data
user = request.user
order_items = data['orderItems']
#1.create order
order = Order.objects.create(
user = user,
total_price = data['totalPrice'],
shipping_price = data['shippingPrice'],
payment_method = data['paymentMethod']
)
#2.create orderItems
for i in order_items:
product = Product.objects.get(id=i['id'])
order_item = OrderItem.objects.create(
order = order,
product = product,
name = i['name'],
qty = i['qty'],
price = i['price'],
image = i['image']
)
#3. update stock
product.stock -= i['qty']
product.save()
#4.create shipping address
shipping_address = ShippingAddress.objects.create(
user = user,
order = order,
address = data['shippingAddress']['address'],
city = data['shippingAddress']['city'],
postal_code = data['shippingAddress']['postalCode'],
country = data['shippingAddress']['country'],
)
#5.serializing and save
serializer = OrderSerializer(order, many=False)
return Response(serializer.data)
You can instantiate the order_items without ever fetching the product, provided you have sufficient trust for the product ids in i['id']
for i in order_items:
# product = Product.objects.get(id=i['id'])
order_item = OrderItem.objects.create(
order = order,
product_id = i['id'], # set the id (magic suffix) without fetching product
name = i['name'],
qty = i['qty'],
price = i['price'],
image = i['image']
)
Instead of using .create you might instantiate these order_items as a list of unsaved instances and create them using OrderItem.bulk_create Read the bulk_create documentation; it has a number of caveats.
You could then run a loop updating the product stock field using an F expression to subtract from the current value in the product row without actually fetching a product object from the DB
for i in order_items:
product_id = i['id']
Product.objects.filter(
pk = product_id
).update(
stock = F('stock') - i['qty']
)
If you do fetch all the product instances into a list with updated stock values, there's also bulk_update which would let you apply all the updated stock values in a single DB operation. This might be better than doing them one by one with an F expression. You can also fetch them in bulk using
Product.objects.filter( pk__in=[ i['id'] for i in order_items ] )
(Warning, I don't think that there's any guarantee that the queryset contains the objects in the same order that you supply the i['id'] values )
Treat this as brainstorming. I'm not entirely certain that this is correct and I really don't know whether it will speed things up a lot, a little, or at all. I'd be interested to know, if you try it.

Django join unmanaged tables

I have a four models which each contains their own data. The models are:
Category (contains department_id foreign key)
Department (contains data, no foreign key)
ProductCategory (join table containing only product_id and category_id)
Product (contains data with no foreign key)
# models.py (excluded the rest for brevity)
from django.db import models
class Department(models.Model):
department_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
description = models.CharField(max_length=1000, blank=True, null=True)
class Meta:
managed = False
db_table = 'department'
class Category(models.Model):
category_id = models.AutoField(primary_key=True)
#department_id = models.IntegerField()
department = models.ForeignKey(Department, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
description = models.CharField(max_length=1000, blank=True, null=True)
class Meta:
managed = False
db_table = 'category'
class Product(models.Model):
product_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
description = models.CharField(max_length=1000)
price = models.DecimalField(max_digits=10, decimal_places=2)
discounted_price = models.DecimalField(max_digits=10, decimal_places=2)
image = models.CharField(max_length=150, blank=True, null=True)
image_2 = models.CharField(max_length=150, blank=True, null=True)
thumbnail = models.CharField(max_length=150, blank=True, null=True)
display = models.SmallIntegerField()
class Meta:
managed = False
db_table = 'product'
class ProductCategory(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Meta:
managed = False
db_table = 'product_category'
unique_together = (('product', 'category'),)
From my endpoint, I need to get all products in a department and return the response in the following format:
​"rows"​: [
{
​ "product_id"​: ​integer​,
"name"​: ​string​,
​"description"​: ​string​,
"price"​: ​string​,
​"discounted_price"​: ​string​,
"thumbnail"​: ​string​
}
]
This is the endpoint:
path('products/inDepartment/<int:department_id>/', ProductViewSet.as_view({"get": "get_products_by_department"}))
How can I go about doing this? I'm stuck with the code below:
# products.py
def get_products_by_department(self, request, department_id):
"""
Get a list of Products of Departments
"""
categories = Category.objects.filter(department_id=department_id).values('category_id')
for item in categories:
category_id = item['category_id']
products = ProductCategory.objects.filter(category_id=category_id).values(
'product_id', name=F('product__name'), description=F('product__description'),
price=F('product__price'), discounted_price=F('product__discounted_price'), thumbnail=F('product__thumbnail'))
# Return response
if products.exists():
return Response(products, 200)
else:
return Response(products, 204)
The code above works and gives me the correct response but I'm not sure if I'm doing the query correctly? Should I be using a loop or is there a Django way to do it better without a loop?
Django's ORM allows for reverse relationship lookup.
https://docs.djangoproject.com/en/2.2/topics/db/queries/#lookups-that-span-relationships
categories = Category.objects.filter(department_id=department_id)
products = Product.objects.filter(productcategory__category__in=categories)

Django: Update model with new foreign_key

I created a form with the model form manager. Before saving my TransactionProfile ModelForm, I want to connect it with an order model. When I print session_order_id it is the correct id, however self.order_set.get is always empty when I print it in the console. Anyone can help me with that? Would you in general solve it the way I did it here, or ist there a more clean method?
In my views.py I have the following:
t = transaction_profile.save(commit=False)
t.update_order_with_transaction_profile(session_order_id)
t.save()
transactions/models.py
class TransactionProfile(models.Model):
email = models.EmailField()
address_line_1 = models.CharField(max_length=120)
address_line_2 = models.CharField(max_length=120, null=True, blank=True)
city = models.CharField(max_length=120)
country = models.CharField(max_length=120)
state = models.CharField(max_length=120)
postal_code = models.CharField(max_length=120)
update = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
customer_id = models.CharField(max_length=120, null=True, blank=True)
def update_order_with_transaction_profile(self, session_order_id):
# In ModelManager or just in class TransactionProfile
o = self.order_set.get(order_id=session_order_id)
o.transaction_profile = self
o.save()
orders/models.py
class Order(models.Model):
order_id = models.CharField(max_length=10, unique=True)
customer_key = models.CharField(max_length=10, unique=True)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
transaction_profile = models.ForeignKey(TransactionProfile, blank=True, null=True, on_delete=models.CASCADE)
You need to save object to DB before using it as foreign key. Since in your code t is not saved in DB, update_order_with_transaction_profile will not work.
Instead of self.order_set, which gives you only orders related to specific profile(empty list for new object), you can directly query on Order model, note you need to save transaction_profile firts:
t = transaction_profile.save()
t.update_order_with_transaction_profile(session_order_id)
def update_order_with_transaction_profile(self, session_order_id):
# In ModelManager or just in class TransactionProfile
o = Order.objects.get(order_id=session_order_id)
o.transaction_profile = self
o.save()

django charge users based on how many counties they want to access

I'm building a web app,
basically I currently have 3 models ,
1- State: which represents all US states
2- County: which represents all counties with foreign key of state
3- Home: which represents all homes with foreign key of County
the app will show homes,
but users needs to subscribe for certain counties (the counties prices can vary)
the goal is : when users subscribe to certain counties they can see the related "Homes" to these counties
I'm not sure how should I represent these relations between users, subscriptions and how to connect it to County model I have.
and how to make a view for the user to add new counties.
Thank you.
Update (My models):
class State(models.Model):
state_name = models.CharField(max_length=50)
def __str__(self):
return self.state_name
class County(models.Model):
county_name = models.CharField(max_length=50)
state = models.ForeignKey(State, on_delete=models.CASCADE)
def __str__(self):
return self.county_name
class Meta:
unique_together = ("county_name", "state")
verbose_name_plural = 'Counties'
class Home(models.Model):
owner_name = models.CharField(max_length=100, null=True, blank=True)
street_address = models.CharField(max_length=100, null=True, blank=True)
city = models.CharField(max_length=50, null=True, blank=True)
county = models.ForeignKey(County, on_delete=models.CASCADE)
postal_code = models.CharField(max_length=50, null=True, blank=True)
price = models.CharField(max_length=50, null=True, blank=True)
sqft = models.CharField(max_length=50, null=True, blank=True)
home_type = models.CharField(max_length=100, null=True, blank=True)
geom = models.PointField()
added = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return '{}, {}, {}'.format(self.street_address, self.city, self.county.state.state_name)
class Meta:
verbose_name = 'Home'
verbose_name_plural = 'Homes'
#property
def state_county(self):
return f'{self.county.county_name}_{self.state}'
#property
def state(self):
return self.county.state.state_name
Here is a basic idea, you should evaluate from this point.
class State(models.Model)
name = models.CharField(max_length=100)
class County(models.Model)
state = models.ForeignKey(State)
name = models.CharField(max_length=100)
class Home(models.Model)
county= models.ForeignKey(County)
name = models.CharField(max_length=100)
class Subscription(models.Model)
county = models.ForeignKey(County)
user = models.ForeignKey(User)
Basically, you can then charge your user per County (observe that one can have more than one County subscription)
Another aproach would be to use a hierarchy to have State>County>Home, on a MPTT, but maybe its not what you want.
One way would be to add ManyToMany County relationship field in the Subscriptions model and then you would query subscribed county and filter Home.
Something in the sense of:
class County(models.Model):
county = models.CharField(max_length=255)
class Home(models.Model):
county = models.ForeignKey(County, on_delete=models.PROTECT)
class Subscription(models.Model):
user = models.ForeingKey(User, on_delete=models.PROTECT)
county = models.ManyToMany(County)
Then you'd query subscriptions and filter based on that.
subscriptions = Subscription.objects.filter(user=request.user).values_list('county', flat=True)
homes = Home.objects.filter(county_id__in=subscriptions)
You could further improved that with models Manager on Subscription to avoid filtering user every time with something like:
class SubscriptionManager(models.Manager):
def user_subscriptions(self, user):
return super().get_queryset().filter(user=user)
class Subscription(models.Model):
user = models.ForeingKey(User, on_delete=models.PROTECT)
county = models.ManyToMany(County)
objects = SubscriptionManager()
and then filter either with:
subscriptions = Subscription.objects.filter(user=request.user).values_list('county', flat=True)
or
subscriptions = Subscription.objects.user_subscriptions(request.user).values_list('county', flat=True)

django many to many field - programmatically retrieve relations

I have been working on a model for tags and am trying to avoid using contenttypes. I have couple questions related to ManyToManyField in django.
I have the following model
taggables/models.py
class Tag(models.Model):
tag_statuses = (
(u'P', _('Pending approval')),
(u'A', _('Approved')),
)
slug = models.SlugField()
created_at = models.DateTimeField(null=True, blank=True)
created_by = models.ForeignKey(User, related_name='tagged_item_created_by')
status = models.CharField(max_length=20, choices=tag_statuses)
site = models.ForeignKey(Site, default=settings.SITE_ID, related_name='tagged_item_site')
def __unicode__(self):
return self.slug
class TagI18n(models.Model):
tag = models.CharField(max_length=100)
descriptor = models.TextField(null=True, blank=True)
# i18n properties
item = models.ForeignKey(Tag)
language = models.CharField(max_length=6, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE)
class Meta:
unique_together = (("language", "item"))
def __unicode__(self):
return self.tag
I also have different apps around my project that uses tag model as many to many field. such as events for example
evetns/models.py
class Item(models.Model):
event_status_list = (
(u'P', _('Pending approval')),
(u'A', _('Approved')),
(u'R', _('Rejected')),
(u'S', _('Spam')),
)
published_at = models.DateTimeField(null=True, blank=True)
published_by = models.ForeignKey(User, null=True, blank=True, related_name='item_published_by')
updated_by = models.ForeignKey(User, null=True, blank=True, related_name='item_updated_by')
updated_at = models.DateTimeField(null=True, blank=True)
site = models.ForeignKey(Site, default=settings.SITE_ID, related_name='events_item_site')
event_slug = models.SlugField(null=True, blank=True)
# event timing
event_start_date = models.DateField()
event_start_time = models.TimeField(null=True, blank=True)
event_end_date = models.DateField()
event_end_time = models.TimeField(null=True, blank=True)
event_recurrent = models.BooleanField(default=False)
event_status = models.CharField(max_length=20, choices=event_status_list, default=u'P')
# relations
media = models.ManyToManyField(ImageFile, null=True, blank=True)
comments = models.ManyToManyField(Comment, null=True, blank=True)
votes = models.ManyToManyField(Vote, null=True, blank=True)
tags = models.ManyToManyField(Tag, null=True, blank=True)
audience = models.ManyToManyField(Audience, null=True, blank=True)
Now what am trying to do here is run a query to programmatically retrieve all the related models to Tag and then count how many a times a tag was used. Am sure I can do that with contenttypes (generic types) but I don't know how it will perform under heavy usage that's why I wanted to do the many to many fields.
If you are interested in the total number of usage ( aka reference count ) of a tag very often, I think you should store it in the database, example put one extra field to the Tag model, like
referencecount = models.IntegerField( default=0 )
Than in the appropriate places, ( example models .save() )you can increment or decrements it's value.
For your use case, the performance of generic wouldn't matter, because you need anyway to do N queries over 2N tables (one for each "taggable" model and one for each m2m join table, at least).
With the m2m approach, you should have the list of 'taggable' models stored somewhere, at least as a list of ('app_name', 'model') pairs. Then use ContentType (it's very performant) to get the actual model class or query directly from there:
counts = {}
for m in taggable_models:
ct = ContentType.get_by_natural_key(*m)
c = ct.model_class().objects.filter(tags=yourtag).distinct().count()
counts[ct.name] = c