Tricky Django QuerySet with Complicated ForeignKey Traversals - python

I am adapting Jim McGaw's e-commerce site from Beginning Django E-Commerce for my client's use. My client sells builds of computers composed of a custom set of parts. The parts that go into a system will change. For instance, the Super Samurai system sold two years from now will have different parts than the one we sell tomorrow. So as we sell each Super Samurai system we need a snapshot of what parts went into the Super Samurai at the moment in time of the sale.
I am having a problem with the QuerySet that copies all of the parts from the table that maps parts to builds (i.e. the parts that go into the Super Samurai)...
class Build(models.Model):
build = models.ForeignKey(PartModel, related_name='+')
part = models.ForeignKey(PartModel, related_name='+')
quantity = models.PositiveSmallIntegerField(default=1)
class Meta:
abstract = True
unique_together = ('build', 'part')
def __unicode__(self):
return self.build.name + ' with ' + str(self.quantity) + ' * ' + \
self.part.family.make.name + ' ' + self.part.name
class BuildPart(Build):
pass
class Meta:
verbose_name = "Build Part"
I need to copy the build parts from the BuildPart table into the OrderBuildPart table...
class OrderBuildPart(Build):
orderItem = models.ForeignKey(OrderItem, unique=False)
class Meta:
verbose_name = "Ordered Build Part"
...so that in the future we know which parts went into so-and-so's build.
McGaw's e-commrece site doesn't allow for items to be bundles of other items. So rather than create some nightmarish scenario of two different tables (and two series of SKUs) for builds and their parts, I wanted a build to be just like any other part...
class PartModel(models.Model):
family = models.ForeignKey(PartFamily)
name = models.CharField("Model Name", max_length=50, unique=True)
slug = models.SlugField(help_text="http://www.Knowele.com/<b>*slug*</b>",
unique=True)
vpn = models.CharField("VPN", help_text="Vendor's Part Number",
max_length=30, blank=True, null=True)
url = models.URLField("URL", blank=True, null=True)
costurl = models.URLField("Cost URL", blank=True, null=True)
cost = models.DecimalField(help_text="How much knowele.com pays", max_digits=9, decimal_places=2, blank=True, null=True)
price = models.DecimalField(help_text="How much a customer pays", max_digits=9, decimal_places=2, blank=True, null=True)
isActive = models.BooleanField(default=True)
isBestseller = models.BooleanField(default=False)
isFeatured = models.BooleanField(default=False)
isBuild = models.BooleanField(default=False)
description = models.TextField(blank=True, null=True)
updated = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
buildpart = models.ManyToManyField('self', through='BuildPart',
symmetrical=False, related_name='+')
class Meta:
ordering = ['name']
verbose_name = "Product Model"
def __unicode__(self):
return self.name
def get_absolute_url(self):
from django.core.urlresolvers import reverse
return reverse('productdetail', args=[self.slug])
The buildpart field references the ManyToMany BuildPart table which allows a build to have many parts and for a part to be associated with many builds.
Through adapting McGaw's code I get pretty much what I need until I finalize the PayPal payment and try to record what parts went into the sold builds at the precise moment of sale...
def payment(request):
token = request.POST['token']
payer = request.POST['payer']
result = paypal.do_express_checkout_payment(request, token, payer)
if result['ACK'][0] in ['Success', 'SuccessWithWarning']:
cart = Cart.objects.get(cart_id=get_cart_id(request))
finalOrder = Order()
finalOrder.cart_id = get_cart_id(request)
finalOrder.token = token
finalOrder.corID = result['CORRELATIONID'][0]
finalOrder.payerID = payer
finalOrder.ipAddress = request.META['REMOTE_ADDR']
finalOrder.first = cart.first
finalOrder.last = cart.last
finalOrder.address = cart.address
finalOrder.email = cart.email
finalOrder.transactionID = result['PAYMENTINFO_0_TRANSACTIONID'][0]
finalOrder.status = 'f'
finalOrder.save()
for item in get_cart_items(request):
oi = OrderItem()
oi.cart_id = item.cart_id
oi.quantity = item.quantity
oi.product = item.product
oi.price = item.price()
oi.save()
if item.product.isBuild:
for part in get_build_parts(request, item):
bp = OrderBuildPart()
bp.build = part.build
bp.part = part.part
bp.quantity = part.quantity
bp.orderItem = oi
bp.save()
empty_cart(request)
return render(request, 'payment.html', locals())
Everything seems fine until we hit the get_build_parts function...
def get_build_parts(request, part):
return BuildPart.objects.filter(build__id=part__product__pk)
...where Django's post-mortem complains "NameError at /payment/ global name 'part__product__pk' is not defined"
How do I traverse these complicated relationships so my boss can look up what parts went into each customer's builds?

The value side of the lookup doesn't work the way you think it does. The double-underscore stuff is for the left-hand side only: in effect, it's a hack to get round Python's syntax requirements. On the right-hand side, you pass a normal expression, which can follow object relationships using the standard dot syntax:
return BuildPart.objects.filter(build__id=part.product.pk)

Try BuildPart.objects.filter(build=part__product) instead of what you have.
Also, it looks like your "post mortem" is coming from the webapp actually being used. You should be learning of problems like this from unit tests failing, not from an HTTP call.

Related

How to iterate through all foreign keys pointed at an object in Django without using _set notation?

I am fairly new to Django, but I am working on an application that will follow a CPQ flow or Configure, Price, Quote. The user should select the product they would like to configure as well as the options to go with it. Once selected, the program should query an external pricing database to calculate price. From there the program should output the pricing & text data onto a PDF quote. I was able to get the application working using the specific product inheriting from a base product class. The issue is now that I've created a second product child class, I cannot use a singular "related_name". I've omitted the lists associated with the drop down fields to help with readability, but I've posted my models.py file below.
Is there a way I can iterate through Product objects that are pointing to a Quote object with a foreign key? A lot of answers I've found on SO relating to this were able to be solved either by specifying the "_set" or "related_name". I've seen other answers use the select_related() method, however, I can't seem to get the query right as the program won't know which set it needs to look at. A quote could have any mix of product instances tied to it, so am unsure how to handle that query. Again, I have been using django under 6 months, so I am a bit green. I am not sure if I am just not fundamentally understanding the big picture here. I thought about instead of using inheritance, to make Product a standalone class and to save the Compact or WRC info to it so I could just use one "related_name", but also thought that would just create another nested layer that would still fail.
Any help would be very appreciated! I've definitely hit the wall.
models.py
class Quote(models.Model):
project_name = models.CharField(max_length=256,blank=True)
customer_first_name = models.CharField(max_length=256,blank=True)
customer_last_name = models.CharField(max_length=256,blank=True)
company_name = models.CharField(max_length=256, blank=True)
address1 = models.CharField(max_length=256, blank=True, help_text ="Address")
address2 = models.CharField(max_length=256, blank=True)
city = models.CharField(max_length=256, blank=True, default="")
state = models.CharField(max_length=256, blank=True, default="")
zip_code = models.CharField(max_length=256, blank=True, default="")
country = models.CharField(max_length=256, blank=True, default="")
phone = PhoneField(blank=True)
email = models.EmailField(max_length=254,blank=True)
grand_total = models.FloatField(default=0)
create_date = models.DateTimeField(default = timezone.now)
class Product(models.Model):
class Meta:
abstract = True
price = models.FloatField(default=0)
total_price = models.FloatField(default=0)
quantity = models.IntegerField()
quote = models.ForeignKey('quote.Quote', on_delete=models.CASCADE)
quantity = models.IntegerField()
class Compact(Product):
base_size = models.CharField(choices=size, max_length = 256)
filter = models.CharField(choices=filter_list, max_length = 256)
product_name = models.CharField(max_length=256,default="Compact")
class WRC(Product):
base_size = models.CharField(choices=size, max_length = 256)
construction = models.CharField(choices=construction_list, max_length = 256)
outlet = models.CharField(choices=outlet_list, max_length = 256)
product_name = models.CharField(max_length=256,default="WRC")
I was able to figure out my issue, but wanted to answer in case someone came across a similar problem as myself. I was able to get get all product objects attached to a quote instance dynamically by modifying the get_context_data() method of my QuoteDetailView. I also needed to use the django library NestedObjects from django.contrib.admin.utils to grab all related objects to the quote instance. I also added a timestamp field to the Product class to be able to sort them. QuoteDetailView copied below.
class QuoteDetailView(FormMixin,DetailView):
model = Quote
form_class = ProductSelectForm
def get_context_data(self, **kwargs):
### collects related objects from quote
collector = NestedObjects(using=DEFAULT_DB_ALIAS)
collector.collect([kwargs['object']])
### slice off first element which is the quote itself
related_objects = collector.nested()
related_objects = related_objects[1:]
### get context data for qoute object
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
### if number of list items is above 0, then add them to the context
### and sort by timestamp
if len(related_objects) != 0:
context['items'] = sorted(related_objects[0], key=lambda x: x.timestamp)
return context

django model field depend on the value of another field

The use case of my application is I will have various fields to fill and among them one is Industry field and another is Segment Field for brand. The industry field is like category that brand falls into. So, if i choose the industry as Health Care for XYZ brand then the segment field should show the items like 'Ayurveda', 'Dental Clinics' (all health care related items). Basically, its like sub-category.
Here is a sample model
class Industry(models.Model):
name = models.CharField(max_length=150, blank=True, null=True)
class Meta:
verbose_name = 'Industry'
verbose_name_plural = 'Industries'
def __str__(self):
return self.name
class Segment(models.Model):
industry = models.ForeignKey(Industry, related_name='segment', on_delete=models.CASCADE)
name = models.CharField(max_length=150, blank=True, null=True)
class Meta:
verbose_name = 'Segment'
verbose_name_plural = 'Segments'
def __str__(self):
return f'{self.industry.name} - {self.name}'
class BusinessModel(models):
industry = models.ForeignKey(Industry, blank=False, null=False, related_name='industry', on_delete=models.CASCADE)
# segements = models.ForeignKey()
total_investment = models.CharField() # will be choice field
This is a simple model and I have not created Segment model as I am not sure how to approach to this problem. I am just curios to know, if for such case, do i have to something special in models.py or in the view side. Such type of things get arise during development phase, thus, I want to be clear on problem solving pattern in django.
UPDATE
https://www.franchisebazar.com/franchisor-registration here if you choose industry inside Business model section, the segment will be updated accordingly.
You can have a 3 model design like
class Industry(models.Model):
name = models.CharField(max_length=150, blank=True, null=True)
class Segment(models.Model):
name = models.CharField(max_length=150, blank=True, null=True)
class Mapping(models.Model):
industry = models.ForeignKey(Industry)
segment = models.ForeignKey(Segment)
You need to define relations between your models. You can find documentation about ManyToMany relation here which is suitable in your case.
you can use ChainedForeginKey.. Check the below links
customizing admin of django to have dependent select fields
https://django-smart-selects.readthedocs.io/en/latest/installation.html

django database structure for a gym members workout management

i would like to develop a django management app for a small gym.
we have a list of members
each member could have 1 or more card
each card could have 1 or more workout
each workout could have 1 or more exercise
each exercise has the following fields:
exercise name (can be choose from a list of exercise names),
series number (can be choose from a list of series),
repetition number (can be choose from a list of repetition),
execution mode (can be choose from a list of executions),
rest time (can be choose from a list of executions).
Here is my possibile implementation of models.py:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Member(models.Model):
#Member data
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
#Contact
email = models.EmailField(blank=True, null=True)
phone = models.CharField(max_length=50, blank=True, null=True)
#Body
birthday = models.DateField(blank=True, null=True)
height = models.IntegerField(blank=True, null=True)
weigth = models.IntegerField(blank=True, null=True)
#Trainer notes
trainer = models.ForeignKey(User, limit_choices_to={'is_staff': True, 'is_superuser': False}, blank=True, null=True)
note = models.CharField(max_length=160, blank=True, null=True)
#Registration status
registration = models.DateField(auto_now_add=True)
expiration = models.DateField(blank=True, null=True)
card = models.OneToOneField('Card')
def __str__(self):
return u'%s %s' % (self.surname, self.name)
class Card(models.Model):
number = models.IntegerField()
#Card status
card_creation = models.DateField(auto_now_add=True)
card_expiration = models.DateField(blank=True, null=True)
workout = models.ForeignKey('Workout')
def __str__(self):
return u'%s' % (self.number)
class Workout(models.Model):
number = models.IntegerField()
exercise = models.ForeignKey('Exercise')
def __str__(self):
return u'%s' % (self.number)
class Exercise(models.Model):
name = models.ForeignKey('Name')
series = models.ForeignKey('Serie')
repetitions = models.ForeignKey('Repetition')
executions = models.ForeignKey('Execution', blank=True, null=True)
rest = models.ForeignKey('Rest')
def __str__(self):
return u'%s' % (self.name)
class Name(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return u'%s' % (self.name)
class Serie(models.Model):
serie = models.CharField(max_length=50)
def __str__(self):
return u'%s' % (self.serie)
class Repetition(models.Model):
repetition = models.IntegerField()
def __str__(self):
return u'%s' % (self.repetition)
class Execution(models.Model):
execution = models.CharField(max_length=50)
def __str__(self):
return u'%s' % (self.execution)
class Rest(models.Model):
rest = models.IntegerField()
def __str__(self):
return u'%s' % (self.rest)
I'm not sure if this works as described. Could you please suggest a possible implementation?
At the end I would like to have only one view with all data: member, card, workout... so the staff user can modify the user and the linked workout card) is it possible with admin.py or do I need a custom admin?
That looks pretty good, and like Samidh T said in the comments you should open up a dev server with sqlite and play around with it. See what's working what's not. That's the best thing you could do.
However, since you asked, here are a couple notes to bear in mind:
"each member could have 1 or more card" - Then why is you card field in Memeber a OneToOne? Wouldn't it make more sense to do a ForeignKey (essentially a ManyToOne) field for Card linking it to member?
"each card could have 1 or more workout" - If you have a ForeignKey from card to workout, then you are actually doing the opposite of what you described, you're doing a Many (cards) for One (workout). But maybe this is what you want? Or maybe you actually want it to be a ManyToManyField? Because each workout probably has different cards and vice-versa. I really can't tell what's the best fit here, but it's something you might want to think over.
"each workout could have 1 or more exercise" Same as with the point before.
I see that every field in your exercise is a ForeingKey. Now again - this is isn't a bad choice or idea, but you should consider what this entails, and consider replacing it with a ManyToMany or, if one of the fields is a static list (i.e. you have a limited list of available names that you know won't change in the future drastically) then you can use a CharField with option choices=.
I can't exactly tell you "this is bad", "this is good", it is a very project-subjective thing, but I hope my few tips helped you in some way. In the end, it all boils down to what you need. If you find yourself confused, I suggest reading a little about table relationships in SQL and how they work.
Django is fantastic for realizing complex relationships, but using it without learning a little bit SQL before can be confusing sometimes because you only see the end result and hardly ever look at the tables themselves.

Django Python m2m_change does not work when removing the relation

I am writing a project related to courses. However, the save() does not work when I use Django admin to add CourseSession. But when I edit it and then save, it will work. Please help me. Followings are class CourseSession and Course. What I am doing here is to update instructors of each course automatically when related course session is added.(update: I have used the m2m_change function but it wont work when removing course session)
def course_session_instructor_changed(sender, instance, action, **kwargs):
superCourse = instance.course
superCourse.instructors.clear()
course_session_set = superCourse.course_session.all()
for each_course_session in course_session_set:
# add instructor
if action=="post_add":
instructors = each_course_session.instructors.all()
for instructor in instructors:
if not instructor in superCourse.instructors.all():
superCourse.instructors.add(instructor)
# remove instructor
elif action=="pre_remove" :
if not each_course_session == instance:
instructors = each_course_session.instructors.all()
for instructor in instructors:
if not instructor in superCourse.instructors.all():
superCourse.instructors.add(instructor)
superCourse.save()
m2m_changed.connect(course_session_instructor_changed, sender=CourseSession.instructors.through)
class CourseSession(models.Model):
course = models.ForeignKey('Course', related_name='course_session')
instructors = models.ManyToManyField(User, related_name = 'instructor_course_session')
enrollment = models.ManyToManyField(User, related_name = 'course_enrollment')
start = models.DateField()
# Weeks of duration
duration = models.IntegerField()
# capacity of the session
max_cap = models.IntegerField()
questionSet = models.ManyToManyField(QuestionSet, blank=True, null=True, related_name='session_questionSet')
class Meta:
verbose_name = _('Session')
verbose_name_plural = _('Sessions')
get_latest_by = "start"
def __unicode__(self):
return unicode(self.instructors.all())+unicode(self.course)+unicode(self.start)
def is_started(self):
return date.today()> self.start
def is_expired(self):
length = timedelta(days = self.duration*7)
return self.start+length< date.today()
def get_enrollment(self):
return self.enrollment.count()
**class Course(models.Model):
name = models.CharField(_('Course Name'),max_length=256)
# Simple Introduction
brief_intro = models.CharField(_('Brief Intro'),max_length=1024)
intro = models.TextField()
learning_obj = models.TextField()
creator = models.ForeignKey(User, related_name = 'course_creator')
created = models.DateTimeField(auto_now_add=True)
cover = models.ImageField(upload_to = 'course/covers/')
institute = models.ForeignKey('Institute', related_name='institute_courses')
workload = models.IntegerField()
assignments = models.IntegerField()
exams = models.IntegerField()
knowledge_tree_root = models.ForeignKey(Topic, related_name='knowledge_tree_root')
instructors = models.ManyToManyField(User, related_name='courses', null=True, blank=True)
#tree_root = models.ForeignKey('Topic')
class Meta:
verbose_name = _('Course')
verbose_name_plural = _('Courses')
def __unicode__(self):
return self.name
It won't work the first time, as many-to-many fields can't be set until the instance has been saved once (since they're saved in a separate table, and have to have an ID to link to). So the admin doesn't set the values until after the save.
Rather than override save, you probably want to use the m2m_changed signal.

How to perform this sql in django model?

SELECT *, SUM( cardtype.price - cardtype.cost ) AS profit
FROM user
LEFT OUTER JOIN card ON ( user.id = card.buyer_id )
LEFT OUTER JOIN cardtype ON ( card.cardtype_id = cardtype.id )
GROUP BY user.id
ORDER BY profit DESC
I tried this:
User.objects.extra(select=dict(profit='SUM(cardtype.price-cardtype.cost)')).annotate(sum=Sum('card__cardtype__price')).order_by('-profit')
But Django automatically added SUM( cardtype.price ) to the GROUP BY clause, and the SQL doesn't run.
Can this be done without raw SQLs?
Provide the model, never mind these Chinese characters :)
class User(models.Model):
class Meta:
verbose_name = "用户"
verbose_name_plural = "用户"
ordering = ['-regtime']
user_status= (
("normal", "正常"),
("deregistered", "注销"),
("locked", "锁定"),
)
name = models.CharField("姓名", max_length=20, db_index=True)
spec_class = models.ForeignKey(SpecClass, verbose_name="专业班级")
idcard = models.CharField("身份证号", max_length=18)
mobileno = models.CharField("手机号", max_length=11)
password = models.CharField("密码", max_length=50) # plain
address = models.CharField("住址", max_length=100)
comment = models.TextField("备注")
certserial = models.CharField("客户证书序列号", max_length=100)
regtime = models.DateTimeField("注册时间", default=datetime.datetime.now)
lastpaytime = models.DateTimeField("上次付款时间", default=datetime.datetime.now)
credit = models.FloatField("信用额度", default=100)
money = models.FloatField("余额", default=0)
use_password = models.BooleanField("使用密码")
use_fetion = models.BooleanField("接收飞信提示")
status = models.CharField("账户状态", choices = user_status, default="normal", max_length=20, db_index=True)
def __unicode__(self):
return self.name
class CardType(models.Model):
class Meta:
verbose_name = "点卡类型"
verbose_name_plural = "点卡类型"
ordering = ['name']
name = models.CharField("类型名称", max_length=20, db_index=True)
note = models.CharField("说明", max_length=100)
offcial = models.BooleanField("官方卡", default=True)
available = models.BooleanField("可用", default=True, db_index=True)
payurl = models.CharField("充值地址", max_length=200)
price = models.FloatField("价格")
cost = models.FloatField("进货价格")
def __unicode__(self):
return u"%s(%.2f元%s)" % (self.name, self.price, u", 平台卡" if not self.offcial else "")
def profit(self):
return self.price - self.cost
profit.short_description = "利润"
class Card(models.Model):
class Meta:
verbose_name = "点卡"
verbose_name_plural = "点卡"
ordering = ['-createtime']
card_status = (
("instock", "未上架"),
("available", "可用"),
("sold", "已购买"),
("invalid", "作废"),
("returned", "退卡"), # sell to the same person !
("reselled", "退卡重新售出"),
)
cardtype = models.ForeignKey(CardType, verbose_name="点卡类型")
serial = models.CharField("卡号", max_length=40)
password = models.CharField("卡密", max_length=20)
status = models.CharField("状态", choices = card_status, default="instock", max_length=20, db_index=True)
createtime = models.DateTimeField("入库时间")
buytime = models.DateTimeField("购买时间", blank=True, null=True)
buyer = models.ForeignKey(User, blank=True, null=True, verbose_name="买家")
def __unicode__(self):
return u'%s[%s]' % (self.cardtype.name, self.serial)
First, one of the outer joins appears to be a bad idea for this kind of thing. Since you provided no information on your model, I can only guess.
Are you saying that you may not have a CARD for each user? That makes some sense.
Are you also saying that some cards don't have card types? That doesn't often make sense. You haven't provided any details. However, if a Card doesn't have a Card Type, I'll bet you have either problems elsewhere in your application, or you've chosen really poor names that don't provide the least clue as to what these things mean. You should fix the other parts of your application to assure that each card actually does have a card type. Or you should fix your names to be meaningful.
Clearly, the ORM statement uses inner joins and your SQL uses outer joins. What's the real question? How to do outer joins correctly?
If you take the time to search for [Django] and Left Outer Join, you'll see that the Raw SQL is a terrible idea.
Or is the real question how to do the sum correctly? From your own answer it appears that the SQL is wrong and you're really having trouble with the sum. If so, please clean up the SQL to be correct.
If the outer joins are part of the problem -- not just visual noise -- then you have to do something like this for an outer join with a sum.
def user_profit():
for u in User.objects.all():
profit = sum[ t.price - t.cost
for c in u.card_set.all()
for t in c.cardtype_set.all() ]
yield user, profit
In your view function, you can then provide the value of function to the template to render the report. Since it's a generator, no huge list is created in memory. If you need to paginate, you can provide the generator to the paginator and everything works out reasonably well.
This is often of comparable speed to a complex raw SQL query with a lot of outer joins.
If, indeed, the card to card-type relationship is not actually optional, then you can shorten this, somewhat. You still have an outer join to think about.
def user_profit():
for u in User.objects.all():
profit = sum[ c.cardtype.price - c.cardtype.cost
for c in u.card_set.all() ]
yield user, profit
Well, I found this
Sum computed column in Django QuerySet
Have to use raw SQL now...
Thank you two!

Categories

Resources