Custom SQL for Geodjango on ForignKey - django

I have a following model:
class UserProfile(models.Model):
user = models.OneToOneField(User)
location = models.PointField(blank=True, null=True, srid=CONSTANTS.SRID)
objects = models.GeoManager()
class Item(models.Model):
owner = models.ForeignKey(UserProfile)
objects = models.GeoManager()
Now I need to sort the Items by distance to some point:
p = Point(12.5807203, 50.1250706)
Item.objects.all().distance(p, field='owner__location')
But that throws me an error:
TypeError: ST_Distance output only available on GeometryFields.
From GeoDjango GeoQuerySet.distance() results in 'ST_Distance output only available on GeometryFields' when specifying a reverse relationship in field_name I can see there is already ticket for this.
Now I don't like the solution proposed in that question since that way I would not get the distance and I would lose the distances.
So I was thinking that I could achieve this by making a custom sql query. I know that this:
UserProfile.objects.distance(p)
will produce something like this:
SELECT (ST_distance_sphere("core_userprofile"."location",ST_GeomFromEWKB('\x0101000020e6100000223fd12b5429294076583c5002104940'::bytea))) AS "distance", "core_userprofile"."id", "core_userprofile"."user_id", "core_userprofile"."verified", "core_userprofile"."avatar_custom", "core_userprofile"."city", "core_userprofile"."location", "core_userprofile"."bio" FROM "core_userprofile"
So my question is: is there some easy way how to manually construct such query that would sort items by distance?

Since the geometry you're measuring distance to is on UserProfile, it makes sense to query for UserProfile objects and then handle each Item object they own. (The distance is the same for all items owned by a profile.)
For example:
all_profiles = UserProfile.objects.all()
for profile in all_profiles.distance(p).order_by('distance'):
for item in profile.item_set.all():
process(item, profile.distance)
You may be able to make this more efficient with prefetch_related:
all_profiles = UserProfile.objects.all()
all_profiles = all_profiles.prefetch_related('item_set') # we'll need these
for profile in all_profiles.distance(p).order_by('distance'):
for item in profile.item_set.all(): # items already prefetched
process(item, profile.distance)
If it's important for some reason to query directly for Item objects, try using extra:
items = Item.objects.all()
items = items.select_related('owner')
distance_select = "st_distance_sphere(core_userprofile.location, ST_GeomFromEWKT('%s'))" % p.wkt
items = items.extra({'distance': distance_select})
items = items.order_by('distance')
Raw queries are another option, which let you get model objects from a raw SQL query:
items = Item.objects.raw("SELECT core_item.* FROM core_item JOIN core_userprofile ...")

Related

Django - sorting object based on user defined order in template

I want the user to be able to order a list of objects in a table using javascript. Then, in a django function I would like to sort those object based on the same ordering, not on an attribute.
Is it possible? I was thinking about passing a list of pk from the template to the view and then ordering the objects according to this list, but I have not found a way to do it yet.
I don't think this is possible with queryset. Try following:
pk_list = [2, 1, 3, 4]
pk2obj = {obj.pk: obj for obj in Model.objects.all()}
objects_ordered = [pk2obj[pk] for pk in pk_list]
pkg2obj is mapping between pk and model instance object. To make a dictionary I used dictionary comprehension.
If you want to omit deleted objects:
objects_ordered = [pk2obj[pk] for pk in pk_list if pk in pk2obj]
Else if you want to replace deleted objects with default value (None in following code):
objects_ordered = [pk2obj.get(pk, None) for pk in pk_list]
I've had to solve this exact problem before.
If you want the user to be able to reorder them into a user-defined order, you can easily define a field to store this order.
As you say, initially, you could serve them in order according to id or an upload_date DateTimeField. But you could also have an PositiveIntegerField in the model, named position or order, to represent the user-defined order.
class MediaItem(models.Model):
user = models.ForeignKey(User)
upload_date = models.DateTimeField(auto_now_add = True)
position = models.PositiveIntegerField()
Whenever a user changes the order on the frontend, the JS can send the new order as an array of objects (ie. new_order = [{"pk":3, "position":1}, {"pk":1, "position":2}, {"pk":2, "position":3}]). The view can look up each instance by pk, and change the position:
for obj in new_order:
media_item = MediaItem.objects.get(pk=obj['pk'])
media_item.position = obj['position']
media_item.save()
Then always query using
objects_ordered.objects.order_by('position')
That's how we managed to do it. If you have more specific questions regarding this approach, feel free to ask in the comments.
Edit:
If the same object can be a member of many different groups or lists, and you want to store the position of the membership within that list, you can achieve this using a through model. A through model is useful when you need to store data that relates to the relationship between two objects that are related. In addition to the MediaItem class shown above, this is what your other models would look like:
class Album(models.Model):
media_items = models.ManyToManyField(MediaItem,
related_name = 'album_media_items',
through = 'Membership')
class Membership(models.Model):
album = models.ForeignKey(Album,
related_name = 'album')
media_item = models.ForeignKey(MediaItem,
related_name = 'media_item')
date = models.DateTimeField(auto_now_add = True)
position = models.PositiveIntegerField()
Then, you could query the Membership instances, instead of the MediaItem instances.
# get id of list, or album...
alb = Album.objects.get(pk=id_of_album)
media_items = Membership.objects.filter(album=alb).order_by('position')
for item in media_items:
# return the items, or do whatever...
# keep in mind they are now in the user-defined order
You can do this:
pk_list = [1,5,3,9]
foo = Foo.objects.filter(id__in=pk_list)
#Order your QuerySet in base of your pk_list using Lambda
order_foo = sorted(foo, key = lambda:x , pk_list.index(x.pk))

Why is get get_queryset() not returning the right results?

I have two models, Recieved_order and order,
class Order(SmartModel):
restaurant = models.ForeignKey(Restaurant,null=True,blank=True,default = None,help_text="The restaurant the customer order from")
#contact info
email = models.EmailField(max_length=50,help_text="Needed as alternative")
mobile = PhoneNumberField(max_length=20,default='+25078######')
class Recieved_Order(SmartModel):
item = models.ForeignKey(Item)
date_added = models.DateTimeField(auto_now=True,auto_now_add=True)
quantity = models.IntegerField(default=0)
price = models.DecimalField(max_digits=9,decimal_places=2)
order = models.ForeignKey(Order)
i want a restaurant manager(user), to be able to receive orders(Recieved_order) made to his specific restaurants when logged in, to achieve this, i have the following in views.py
class Recieved_OrderCRUDL(SmartCRUDL):
model = Recieved_Order
actions = ('create','read','update','delete','list')
permissions = True
class List(SmartListView):
fields = ('order_email','order_mobile','order_billing_city','item.name','item.price','quantity','order_id','order_restaurant')
search_fields = ('date_added',)
def get_queryset(self,*args,**kwargs):
queryset = super(Recieved_OrderCRUDL.List, self).get_queryset(*args,**kwargs)
if self.request.user.is_superuser:
return queryset
return queryset.filter(order=self.request.user)
with the above i am testing on two different restaurants, the restaurant and its not working out as it should. its returning the wrong orders for a given restaurant.
What am i not doing right with get_queryset().
There's something confusing going on here:
return queryset.filter(order=self.request.user)
You're telling it to build a query that filters Order objects against User objects.
Is there something missing in your sample code that ties orders back to users such that a proper join can be constructed?
If you want to have a user (what you refer to as a manager) only able to view their own orders, you need to change things... Restaurant will need to have a field that points to a User (let's call it user and assume it's a ForeignKey) Then you can do something like
if self.request.user.is_superuser:
return queryset
return queryset.filter(order__restaurant__user=self.request.user)
As pointed out by #Joe Holloway, you should not be trying to filter on the order field with a user object...
The other odd thing I wanted to point out is
fields = ('order_email','order_mobile','order_billing_city','item.name','item.price','quantity','order_id','order_restaurant')
You appear to be using a mixture of ways to attempt to access things...
You should be using __ (that's 2 underscores) to access relations, not _ or .

Sorting by distance with a related ManyToMany field

I have this two models.
class Store(models.Model):
coords = models.PointField(null=True,blank=True)
objects = models.GeoManager()
class Product(models.Model):
stores = models.ManyToManyField(Store, null=True, blank=True)
objects = models.GeoManager()
I want to get the products sorted by the distance to a point. If the stores field in Product was a Foreign Key I would do this and it works.
pnt = GEOSGeometry('POINT(5 23)')
Product.objects.distance(pnt, field_name='stores__coords').order_by('distance')
But since the field is a ManyToMany field it breaks with
ValueError: <django.contrib.gis.db.models.fields.PointField: coords> is not in list
I kind of expected this because it's not clear which of the stores it should use to calculate the distance, but is there any way to do this.
I need the list of products ordered by distance to a specific point.
Just an idea, maybe this would work for you, this should take only two database queries (due to how prefetch works). Don't judge harshly if it doesn't work, I haven't tried it:
class Store(models.Model):
coords = models.PointField(null=True,blank=True)
objects = models.GeoManager()
class Product(models.Model):
stores = models.ManyToManyField(Store, null=True, blank=True, through='ProductStore')
objects = models.GeoManager()
class ProductStore(models.Model):
product = models.ForeignKey(Product)
store = models.ForeignKey(Store)
objects = models.GeoManager()
then:
pnt = GEOSGeometry('POINT(5 23)')
ps = ProductStore.objects.distance(pnt, field_name='store__coords').order_by('distance').prefetch_related('product')
for p in ps:
p.product ... # do whatever you need with it
This is how I solved it but I dont really like this solution. I think is very inefficient. There should be a better way with GeoDjango. So, until i find a better solution I probably wont be using this. Here's what I did.
I added a new method to the product model
class Product(models.Model):
stores = models.ManyToManyField(Store, null=True, blank=True)
objects = models.GeoManager()
def get_closes_store_distance(point):
sorted_stores = self.stores.distance(point).order_by('distance')
if sorted_stores.count() > 0:
store = sorted_stores[0]
return store.distance.m
return 99999999 # If no store, return very high distance
Then I can sort this way
def sort_products(self, obj_list, lat, lng):
pt = 'POINT(%s %s)' % (lng, lat)
srtd = sorted(obj_list, key=lambda obj: obj.get_closest_store_distance(pt))
return srtd
Any better solutions or ways to improve this one are very welcome.
I will take "distance from a product to a point" to be the minimum distance from the point to a store with that product. I will take the output to be a list of (product, distance) for all products sorted by distance ascending. (A comment by someone who placed a bounty indicated they sometimes also want (product,distance,store) sorted by distance then store within product.)
Every model has a corresponding table. The fields of the model are the columns of the table. Every model/table should have a fill-in-the-(named-)blanks statement where its records/rows are the ones that make a true statement.
Store(coords,...) // store [store] is at [coords] and ...
Product(product,store,...) // product [product] is stocked by store [store] and ...
Since Product has store(s) as manyToManyField it already is a "ProductStore" table of products and stocking stores and Store already is a "StoreCoord" table of stores and their coordinates.
You can mention any object's fields in a query filter() for a model with a manyToManyField.
The SQL for this is simple:
select p.product,distance
select p.product,distance(s.coord,[pnt]) as distance
from Store s join Product p
on s.store=p.store
group by product
having distance=min(distance)
order by distance
It should be straightforward to map this to a query. However, I am not familiar enough with Django to give you exact code now.
from django.db.models import F
q = Product.objects.all()
.filter(store__product=F('product'))
...
.annotate(distance=Min('coord.distance([pnt])'))
...
.order_by('distance')
The Min() is an example of aggregation.
You may also be helped by explicitly making a subquery.
It is also possible to query this by the raw interface. However, the names above are not right for a Django raw query. Eg the table names will by default be APPL_store and APPL_product where APPL is your application name. Also, distance is not your pointField operator. You must give the right distance function. But you should not need to query at the raw level.

Performing a Django Query on a Model, But Ending Up with a QuerySet for That Model's ManyToManyField

I have a third party Django App (Satchmo) which has a model called Product which I make extensive use of in my Django site.
I want to add the ability to search for products via color. So I have created a new model called ProductColor. This model looks roughly like this...
class ProductColor(models.Model):
products = models.ManyToManyField(Product)
r = models.IntegerField()
g = models.IntegerField()
b = models.IntegerField()
name = models.CharField(max_length=32)
When a store product's data is loaded into the site, the product's color data is used to create a ProductColor object which will point to that Product object.The plan is to allow a user to search for a product by searching a color range.
I can't seem to figure out how to put this query into a QuerySet. I can make this...
# If the color ranges look something like this...
r_range, g_range, b_range = ((3,130),(0,255),(0,255))
# Then my query looks like
colors_in_range = ProductColor.objects.select_related('products')
if r_range:
colors_in_range = colors_in_range.filter(
Q(r__gte=r_range[0])
| Q(r__lte=r_range[1])
)
if g_range:
colors_in_range = colors_in_range.filter(
Q(g__gte=g_range[0])
| Q(g__lte=g_range[1])
)
if b_range:
colors_in_range = colors_in_range.filter(
Q(b__gte=b_range[0])
| Q(b__lte=b_range[1])
)
So I end up with a QuerySet which contains all of the ProductColor objects in that color range. I could then build a list of Products by accessing the products ManyToMany attribute of each ProductColor attribute.
What I really need is a valid QuerySet of Products. This is because there is going to be other logic which is performed on these results and it needs to operate on a QuerySet object.
So my question is how can I build the QuerySet that I really want? And failing that, is there an efficient way to re-build the QuerySet (preferably without hitting the database again)?
If you want to get a Product queryset you have to filter the Product objects and filter via the reverse relation for product color:
products = Product.objects.filter(productcolor_set__r__gte=x).distinct()
You can use the range field lookup:
You can use range anywhere you can use
BETWEEN in SQL -- for dates, numbers
and even characters.
your query:
r_range, g_range, b_range = ((3,130),(0,255),(0,255))
products = Product.objects.filter(productcolor_set__r__range=r_range,
productcolor_set__g__range=g_range,
productcolor_set__b__range=b_range).distinct()

Fetching ManyToMany objects from multiple objects through intermediate tables

Is there an easy way to fetch the ManyToMany objects from a query that returns more than one object? The way I am doing it now doesn't feel as sexy as I would like it to. Here is how I am doing it now in my view:
contacts = Contact.objects.all()
# Use Custom Manager Method to Fetch Each Contacts Phone Numbers
contacts = PhoneNumber.objects.inject(contacts)
My Models:
class PhoneNumber(models.Model):
number = models.CharField()
type = models.CharField()
# My Custom Manager
objects = PhoneNumberManager()
class Contact(models.Model):
name = models.CharField()
numbers = models.ManyToManyField(PhoneNumber, through='ContactPhoneNumbers')
class ContactPhoneNumbers(models.Model):
number = models.ForeignKey(PhoneNumber)
contact = models.ForeignKey(Contact)
ext = models.CharField()
My Custom Manager:
class PhoneNumberManager(models.Manager):
def inject(self, contacts):
contact_ids = ','.join([str(item.id) for item in contacts])
cursor = connection.cursor()
cursor.execute("""
SELECT l.contact_id, l.ext, p.number, p.type
FROM svcontact_contactphonenumbers l, svcontact_phonenumber p
WHERE p.id = l.number_id AND l.contact_id IN(%s)
""" % contact_ids)
result = {}
for row in cursor.fetchall():
id = str(row[0])
if not id in result:
result[id] = []
result[id].append({
'ext': row[1],
'number': row[2],
'type': row[3]
})
for contact in contacts:
id = str(contact.id)
if id in result:
contact.phonenumbers = result[id]
return contacts
There are a couple things you can do to find sexiness here :-)
Django does not have any OOTB way to inject the properties of the through table into your Contact instance. A M2M table with extra data is a SQL concept, so Django wouldn't try to fight the relations, nor guess what should happen in the event of namespace collision, etc... . In fact, I'd go so far as to say that you probably do not want to inject arbitrary model properties onto your Contact object... if you find yourself needing to do that, then it's probably a sign you should revise your model definition.
Instead, Django provides convenient ways to access the relation seamlessly, both in queries and for data retrieval, all the while preserving the integrity of the entities. In this case, you'll find that your Contact object offers a contactphonenumbers_set property that you can use to access the through data:
>>> c = Contact.objects.get(id=1)
>>> c.contactphonenumbers_set.all()
# Would produce a list of ContactPhoneNumbers objects for that contact
This means, in your case, to iterate of all contact phone numbers (for example) you would:
for contact in Contact.objects.all():
for phone in contact.contactphonenumbers_set.all():
print phone.number.number, phone.number.type, phone.ext
If you really, really, really want to do the injection for some reason, you'll see you can do that using the 3-line code sample immediately above: just change the print statements into assignment statements.
On a separate note, just for future reference, you could have written your inject function without SQL statements. In Django, the through table is itself a model, so you can query it directly:
def inject(self, contacts):
contact_phone_numbers = ContactPhoneNumbers.objects.\
filter(contact__in=contacts)
# And then do the result construction...
# - use contact_phone_number.number.phone to get the phone and ext
# - use contact_phone_number.contact to get the contact instance