Implementing Django ForeignKey-like subqueries without ForeignKey - django

I have a non-ForeignKey ID from an external system that acts as my join key. I'd like to do Django ORM style queries using this ID.
My desired query is:
results = MyModel.objects.filter(level='M', children__name__contains='SOMETHING')
My model looks like this:
class MyModel(BaseModel):
LEVELS = (
('I', 'Instance'),
('M', 'Master'),
('J', 'Joined')
)
level = models.CharField(max_length=2, choices=LEVELS, default='I')
parent = models.ForeignKey('self', blank=True, null=True, related_name='children', on_delete=models.SET_NULL )
master_id = models.CharField(max_length=200)
name = models.CharField(max_length=300, blank=True, null=True)
This works fine with parent as a field, but parent is redundant with the master_id field: master_id indicates which children belong to which master node. I'd like to get rid of parent (primarily because the dataset is fairly large and setting the parent IDs when importing data takes a long time).
The SQL equivalent of what I'm looking for is:
SELECT
DISTINCT( s_m.master_id )
FROM
mytable s_m JOIN
mytable s_i ON
s_i.level = 'I' and s_m.level='M' AND s_i.master_id == s_m.master_id
WHERE
s_i.name like '%SOMETHING%';
I believe there's a way to use Manager or QuerySet to enable clean querying of children (in this case, the children's names) within the Django ORM framework, but I can't figure out how. Any pointers would be appreciated.

Can you try something like this?
table1.objects.filter(master_id__in=table2.objects.filter(level='I').values_list(master_id,flat=True),level='M',name__contains='SOMETHING').values_list(master_id).distinct()

Related

Update column in Django with child columns

I have 2 models Parent, Child
class Parent(models.Model):
id = Base64UUIDField(primary_key=True, editable=False)
cost = models.DateTimeField(default=None, blank=True, null=True)
class Child(models.Model):
id = Base64UUIDField(primary_key=True, editable=False)
cost = models.DateTimeField(default=None, blank=True, null=True)
parent = models.ForeignKey(Parent, related_name= "children", related_query_name= "child")
I need to populate cost column of Parent objects to maximum cost of all children of that parent
I have tried to annotate to a new column new_cost, and its works.
parents.annotate(new_cost=Max('child__cost'))
But I need to populate values to existing column cost. Tried something like this, but not working.
parents.update(cost=Max('child__cost'))
Probably that can be achieved with a Subquery expression:
from django.db.models import OuterRef, Subquery
Parent.objects.update(
cost=Subquery(Child.objects.filter(
parent_id=OuterRef('pk')
).values('cost').order_by('-cost')[:1])
)
I would however advise to simply use .annotate(…) when you need the largest cost of the related Childs.
Use aggregate againist to annotate
test = parents.aggregate(new_cost=Max('child__cost'))
parents.update(cost=test["new_cost"])

Django annotate subquery's aggregation

So I have three models
class Advert(BaseModel):
company = models.ForeignKey(Company, on_delete=CASCADE, related_name="adverts")
class Company(BaseModel):
name = models.CharField(max_length=50)
class OrderRating(BaseModel):
reported_company = models.ForeignKey(Company, on_delete=CASCADE, related_name='ratings')
rating = models.DecimalField(
max_digits=2,
decimal_places=1,
validators=[MinValueValidator(1.0), MaxValueValidator(5.0)],
help_text='Rating from 1.0 to 5.0.'
)
And I'm trying to get average of all order ratings related to the company and annotate that to the Advert model, when I do this:
qs = Advert.objects.all().annotate(
avg_rating=Subquery(
OrderRating.objects.filter(
reported_company=OuterRef('company')).aggregate(Avg("rating"))["rating__avg"]
)
)
I get back stating
This queryset contains a reference to an outer query and may only be used in a subquery.'
Not sure where the problem is when I am calling the OuterRef inside a Subquery.
According to my experience Subqueries are often a bit tricky and not well documented. And they tend to return the message you are receiving when there is some error in your code defining the Subquery (not a very helpful message indeed).
As far as I know aggregate does not work in Subequeries, you must use annotations instead. So this should work:
qs = Advert.objects.all().annotate(
avg_rating=Subquery(
OrderRating.objects.filter(
reported_company=OuterRef('company')).values('reported_company').annotate(av=Avg('rating')).values('av')
)
)

Django - Join on specific column

I have a model, let's call it Notification. A notification has a from_user, and a to_user among other fields which are not important right now.
class Notification(models.Model):
from_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='from_user'
)
to_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='to_user'
)
read = models.BooleanField(default=False)
...
What I want to achieve is, in theory, very simple, but I can not get it to work.
I would like to get the count of unread notifications for every user.
The following Django ORM expression:
User.objects.annotate(
notification_count=Count('notification', filter=(Q(notification__read=False)))
).values('notification_count')
Produces the following SQL:
SELECT COUNT("notification"."id") FILTER (WHERE "notification"."read" = False) AS "notification_count"
FROM "account"
LEFT OUTER JOIN "notification" ON ("account"."id" = "notification"."from_user_id")
GROUP BY "account"."id"
ORDER BY "account"."member_since" DESC
But this is not what I want. I want the LEFT_OUTER_JOIN to be done on the to_user column, not the from_user column.
How can I achieve this using the Django ORM?
I found out the solution after trying endless things. Here it is, for anyone who needs it:
User.objects.annotate(
notification_count=Count('to_user', filter=(Q(to_user__read=False)))
).values('notification_count')

Python/Django recursion issue with saved querysets

I have user profiles that are each assigned a manager. I thought using recursion would be a good way to query every employee at every level under a particular manager. The goal is, if the CEO were to sign in, he should be able to query everyone at the company - but If I sign on I can only see people in my immediate team and the people below them, etc. until you get to the low level employees.
However when I run the following:
def team_training_list(request):
# pulls all training documents from training document model
user = request.user
manager_direct_team = Profile.objects.filter(manager=user)
query = Profile.objects.filter(first_name='fake')
trickle_team = manager_loop(manager_direct_team, query)
# manager_trickle_team = manager_direct_team | trickle_team
print(trickle_team)
def manager_loop(list, query):
for member in list:
user_instance = User.objects.get(username=member)
has_team = Profile.objects.filter(manager=user_instance)
if has_team:
query = query | has_team
manager_loop(has_team, query)
else:
continue
return query
It only returns the last query that was run instead of the compiled queryset that I am trying to grow. I've tried placing 'return' before 'manager_loop(has_team, query) in order save the values but it also kills the loop at the first non-manager employee instead of continuing to the next employee.
I'm new to django so if there is an better way than recursion to pull the information that I need, I'd appreciate suggestions on that too.
EDIT:
As requested, here is the profile model.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=30, blank=False)
last_name = models.CharField(max_length=30, blank=False)
email = models.EmailField( blank=True, help_text='Optional',)
receive_email_notifications = models.BooleanField(default=False)
mobile_number = models.CharField(
max_length=15,
blank=True,
help_text='Optional'
)
carrier_options = (
(None, ''),
('#txt.att.net', 'AT&T'),
('#messaging.sprintpcs.com', 'Sprint'),
('#tmomail.net', 'T-Mobile'),
('#vtext.com', 'Verizon'),
)
mobile_carrier = models.CharField(max_length=25, choices=carrier_options, blank=True,
help_text='Optional')
receive_sms_notifications = models.BooleanField(default=False)
job_title = models.ForeignKey(JobTitle, unique=False, null=True)
manager = models.ForeignKey(User, unique=False, blank=True, related_name='+', null=True)
Ok, so it's a hierarchical model.
The problem with your current approach is this line:
query = query | has_team
This reassigns the local name query to a new queryset, but does not reassign the name in the caller. (Well, that's what I think it's trying to do - I am a little rusty but I don't think you can just | together querysets like that.) You'd also need something like:
query = manager_loop(has_team, query)
to propagate the changes via the returned object.
That said, while Django doesn't have built-in support for recursive queries, there are some third party packages that do. Old answers eg (Django self-recursive foreignkey filter query for all childs and Creating efficient database queries for hierarchical models (django)) recommend django-mptt. Your tag mentions postgres, so this post might be relevant:
https://two-wrongs.com/fast-sql-for-inheritance-in-a-django-hierarchy
If you don't use a third-party approach, it should be possible to clean up the evolution of the queryset - cast it to a set and use update or something, since you're accumulating profiles. But the key error is not using the returned modified object.

Django: how to design the following model?

I'd like to know how I can design my Django model to achieve the following:
Road -> Category (required): Highway (select list)
Road -> Attribute (optional): Traffic -> Heavy + Moderate (checkboxes)
Road -> Attribute (optional): Condition -> Smooth + Rough + Average(checkboxes)
Does it make sense to include TRAFFIC_CHOICES, CONDITION_CHOICES under the Road class vs creating separate classes for each set of choices vs creating a generic Attribute class?
How do I display choices as checkboxes?
The end goal of this model is to be able to create queries such as "Highway roads that are smooth with no traffic"
Here is my attempt:
class Category(models.Model):
CATEGORY_CHOICES = (
('highway', 'Highway'),
('parkway', 'Parkway'),
)
name = models.CharField(max_length=1, choices=CATEGORY_CHOICES, blank=False)
class Road(models.Model):
name = models.TextField(blank=False)
TRAFFIC_CHOICES = (
('moderate', 'Moderate'),
('busy', 'Busy'),
)
traffic = models.CharField(max_length=1, choices=TRAFFIC_CHOICES)
CONDITION_CHOICES = (
('smooth', 'Smooth'),
('rough', 'Rough'),
('average', 'Average'),
)
condition = models.CharField(max_length=1, choices=CONDITION_CHOICES)
First, change the first models.TextField to a CharField like the others.
Category does not have to be a separate model, unless you intend to add new categories after your application is finished, in which case it must be a separate model and you should use a ForeignKey relationship from the Road to the Category and ditch the CATEGORY_CHOICES.
Assuming you do not ever intend to add new categories, you can get rid of the Category model entirely and put the CATEGORY_CHOICES into Road. Then change name = to category = and put that into Road too.
You have a max_length of 1 for all of those fields, which is fine, but in that case you need to make the CHOICES map to single characters so they fit in the field. For instance:
CATEGORY_CHOICES = (
('H', 'Highway'),
('P', 'Parkway'),
)
Why do you want to use checkboxes for your traffic and condition choices? Checkboxes mean that you can select multiple answers instead of being forced to select just one. This doesn't make sense conceptually for road condition or traffic, and it isn't compatible with a CharField (because a CharField can only store one value, without some very contrived setup). You could keep the system you have and use radio buttons if you prefer them over dropdowns, but you can't use checkboxes without getting rid of the choices and instead making each possible checked box its own BooleanField or NullBooleanField.
I usually put my choices outside of the class definition. I don't know if it works how you expect it to work when it's inside. I'm going to move them outside the class definition for my example; this may not be required, so feel free to experiment.
To summarize (and I'm changing the name field to a CharField because TextField is only for really large blocks and not for names):
CATEGORY_CHOICES = (
('H', 'Highway'),
('P', 'Parkway'),
)
TRAFFIC_CHOICES = (
('M', 'Moderate'),
('B', 'Busy'),
)
CONDITION_CHOICES = (
('S', 'Smooth'),
('R', 'Rough'),
('V', 'Varying'),
)
class Road(models.Model):
name = models.CharField(max_length=512, blank=False)
category = models.CharField(max_length=1, choices=CATEGORY_CHOICES, blank=False)
traffic = models.CharField(max_length=1, choices=TRAFFIC_CHOICES)
condition = models.CharField(max_length=1, choices=CONDITION_CHOICES)
Edit: If you really are sure that checkboxes are the best fit, then you have two main choices. As above, if you intend to add choices later after your application is finished, then your strategy is different (you need to use a ManyToManyField). Otherwise you could implement them like this:
class Road(models.Model):
name = models.CharField(max_length=512, blank=False)
category = models.CharField(max_length=1, choices=CATEGORY_CHOICES, blank=False)
moderate_traffic = models.NullBooleanField()
heavy_traffic = models.NullBooleanField()
smooth_condition = models.NullBooleanField()
rough_condition = models.NullBooleanField()
varying_condition = models.NullBooleanField()
The part where you display them as grouped checkboxes specifically happens when you're displaying your form, not in the model.
You could also use some kind of bit field, but that's not included in Django by default -- you'd have to install an extension.