Reputation: 103
I am trying to figure out a tricky Django query that I hope you can help with. I have this model:
class ActiveVenue(models.Model):
event = models.ForeignKey(Event)
venue = models.ForeignKey(Venue)
class Venue(models.Model):
name = models.CharField(max_length=200)
class Event(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=200)
In my app there are many Events and each event can have multiple active venues, hence my current data structure. I would prefer if your solution is not "change your model to foo" as this is already a deployed site and I would like to keep the current model structure.
I want to write a query that returns the most popular venues but in which a venue is only counted once for each user. As an example, if I have one user with four events and they use the same venue every time, I only want to count that venue once when determining the most popular venues.
To illustrate this, imagine this is my data:
event: A user: bob venue: The Hill
event: B user: bob venue: The Hill
event: C user: bob venue: The Hill
event: D user: jane venue: The Oaks
event: E user: sarah venue: The Pound
event: F user: david venue: The Pound
event: G user: ron venue: The Oaks
event: H user: erica venue: The Oaks
Here the popular order would be:
1. The Oaks
2. The Pound
3. The Hill
Any suggestions how I write a query to do this that works on both postgres and sqlite (in other words, doesn't rely on distinct()
(not supported in sqlite))?
Thanks!
Upvotes: 4
Views: 374
Reputation: 1
Here is my take on the problem. First, here is a query that will grab the Venue ids and the scores corresponding to the related users.
testquery = ActiveVenue.objects.values("venue").annotate(score=Count("event__user", distinct=True)).order_by("-score")
Result
[{'score': 3, 'venue': 2}, {'score': 2, 'venue': 3}, {'score': 1, 'venue': 1}]
Next, the venue ids will be placed in a new list.
query_ids = [item["venue"] for item in testquery]
[2, 3, 1]
Next, the corresponding Venue objects have to be obtained.
tempresult = Venue.objects.filter(id__in=query_ids)
[<Venue: The Hill>, <Venue: The Oaks>, <Venue: The Pound>
Finally, a list comprehension will be performed to re-order the Venue objects based on the scores obtained earlier.
result = [venue for venue_id in query_ids for venue in tempresult if venue_id == venue.id]
[<Venue: The Oaks>, <Venue: The Pound>, <Venue: The Hill>]
This gives the proper result based on the test data.
Upvotes: 0
Reputation: 22808
This is my version (finish test):
models.py
class Venue(models.Model):
name = models.CharField(max_length=200)
def __unicode__(self):
return self.name
def ranking(self):
count=0
actives = ActiveVenue.objects.filter(
venue__name=self.name).values(
'event__user__username', 'venue__name').distinct()
for active in actives:
count += 1
return count
views.py
def getRanking( anObject ):
return anObject.ranking()
def myview(request):
venues = list(Venue.objects.filter())
venues.sort(key=getRanking, reverse=True)
return render(request,'page.html',{'venues': venues})
templates
{% for venue in venues %}
{{forloop.counter}}. {{venue}}<br/>
{% endfor %}
Output:
Upvotes: 0
Reputation: 698
See if something like this works
ActiveVenue.objects.all().annotate(score=Count('event__user', distinct=True)).order_by('-score')
Upvotes: 0
Reputation: 43840
Does this work?
from collections import Counter
results = Counter([vid for vid, eid in ActiveVenue.objects.values_list("venue_id", "event_id").distinct()]
Upvotes: 1