In my application I have models Visits & Post &
class Visit < ActiveRecord::Base
belongs_to :post, :counter_cache => true
class Post < ActiveRecord::Base
has_many :visits
When a visitor visits a post, I am adding it to my visits table with post_id and price (price is decimal).
In my dashboard, I want to show which posts they have viewed (grouped) and how much they have earned.
For instance:
1) post 1, viewed 54 times and earned $1.6, 2) post 2, viewed 39 times and earned $1.1, etc
I have tried with:
- a = Visit.group(:post_id).where(user: current_user).sum(:price)
- a.each do |n|
%p
= n
This gives me, each post_id, but price is just * BigDecimal:7fb2625f9238,'0.15E1* & I can't find post title by doing n.post.title & n.post.title gives me error: undefined method 'post'
This is result I get:
[44, #<BigDecimal:7fb2625f9238,'0.15E1',18(36)>]
[45, #<BigDecimal:7fb2625f8dd8,'0.13E1',18(36)>]
[46, #<BigDecimal:7fb2625f8928,'0.3E-1',9(36)>]
I have also tried with:
- Visit.select([:post_id, :price]).where(user: current_user).each do |e|
%p
= e.post.title
= e.cpc_bid
This option gives me all the posts and prices individually and not combined.
Results are like:
Post title 1, 0.15
Post title 1, 0.01
Post title 2, 0.1
Post title 1, 0.15
Post title 2, 0.1
Post title 2, 0.1
Post title 2, 0.1
Post title 1, 0.15
I also tried with:
- Visit.select([:post_id, :price]).group(:post_id).where(user: current_user).each do |e|
%p
= e.post.title
= e.price
This option gives me only one of the visits on the post with its price.
Results are:
Post title 2, 0.1
Post title 1, 0.15
My last try was:
- Visit.joins(:post).group(:post_id).select('sum(price) as earnings', :post_id, :title, 'count(visits.id) as total_views').where(user: current_user).each do |e|
%p
= e.title
= e.price
This gives me this error:
PG::GroupingError: ERROR: column "posts.title" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...ECT sum(price) as earnings, "visits"."post_id", "title", c...
How can I combine them together with sum of price on all post, with its post title.
You need to join tables and group
Post.joins(:visits).group(:id)
.where(visits: { user_id: current_user.id})
.select("*, sum(price) as total_price, count(visits.id) as total_views")
It adds to post instance accessors total_price and total_views.
Answer from #MikDiet works perfectly, only issue I had was that I was getting error from PostgreSql:
PG::GroupingError: ERROR: column "posts.title" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...ECT sum(price) as earnings, "visits"."post_id", "title", c...
and I changed it to:
Visit.joins(:post).group([:post_id, :title]).select('sum(cpc_bid) as earnings', :post_id, :title, 'count(visits.id) as total_views').where(influencer: current_user)
And it worked.
NB: I couldn't find the answer without #MikDiet's help and all credits goes to him
Related
I'm retrieving all records, and I would like to display the record's age for those records that are older than 5 minutes.
The output should be something like this (in this example, two records: 1.8.9.1 and 2.7.3.1 are older than 5 minutes) :
ip ... status
---------------------
1.8.9.1 ... 3 hours
2.7.3.1 ... 7 minutes
1.1.1.1 ... up
1.1.1.2 ... up
1.1.1.3 ... up
1.1.1.4 ... up
1.1.1.5 ... up
Here's my current code:
Interfaces.objects.all()
.annotate(
age = (datetime.utcnow() - F('timestamp')), # 0:00:08.535704
age2 = Epoch(datetime.utcnow() - F('timestamp')), # 8.535704
# age3 = int(Epoch(datetime.utcnow() - F('timestamp'))/300),
current_time=Value(str(datetime.utcnow()),
output_field=null_char_field),
)
.order_by('age','ip')
age and age2 both work, but the problem is that I want the records that are older than 5 minutes sorted by age, and the rest by ip
So I'm trying to set age to 0, if it's less than 5 minutes.
If I would do it directly in postgresql, I'd use this query:
select ip, <other fields>,
case when extract('epoch' from now() - "timestamp") > 300
then extract('epoch' from now() - "timestamp")
else 0
end
Is there a way to do it in django?
I figured it out:
Interfaces.objects.all()
.annotate(
age=Case(
When(timestamp__lt=datetime.utcnow() - timedelta(minutes=5),
then=Cast(Epoch(datetime.utcnow() - F('timestamp')),
NullIntegerField)),
default=0,
output_field=NullIntegerField
),
)
.order_by('age','ip')
By the way, my imports and relevant settings:
from django.db.models import F, Func, Case, When, IntegerField
from django.db.models.functions import Coalesce, Cast
NullIntegerField = IntegerField(null=True)
class Epoch(Func):
function = 'EXTRACT'
template = "%(function)s('epoch' from %(expressions)s)"
This website ended up being the most helpful: https://micropyramid.com/blog/django-conditional-expression-in-queries/
You can do it in other way also which will be faster.
Get current time, subtract from that 5 minutes, after that search all the Interfaces
where age is less or equal than the subtracted date.
example:
current_time = datetime.now()
older_than_five = current_time - datetime.timedelta(minutes=5)
Interfaces.objects.all()
.annotate(
age=Case(
When(age__lt=older_than_five, then=Value(0)),
default=F('age')
)
)
.order_by('age','ip')
I want to join the sum of related values from users with the users that do not have those values.
Here's a simplified version of my model structure:
class Answer(models.Model):
person = models.ForeignKey(Person)
points = models.PositiveIntegerField(default=100)
correct = models.BooleanField(default=False)
class Person(models.Model):
# irrelevant model fields
Sample dataset:
Person | Answer.Points
------ | ------
3 | 50
3 | 100
2 | 100
2 | 90
Person 4 has no answers and therefore, points
With the query below, I can achieve the sum of points for each person:
people_with_points = Person.objects.\
filter(answer__correct=True).\
annotate(points=Sum('answer__points')).\
values('pk', 'points')
<QuerySet [{'pk': 2, 'points': 190}, {'pk': 3, 'points': 150}]>
But, since some people might not have any related Answer entries, they will have 0 points and with the query below I use Coalesce to "fake" their points, like so:
people_without_points = Person.objects.\
exclude(pk__in=people_with_points.values_list('pk')).\
annotate(points=Coalesce(Sum('answer__points'), 0)).\
values('pk', 'points')
<QuerySet [{'pk': 4, 'points': 0}]>
Both of these work as intended but I want to have them in the same queryset so I use the union operator | to join them:
everyone = people_with_points | people_without_points
Now, for the problem:
After this, the people without points have their points value turned into None instead of 0.
<QuerySet [{'pk': 2, 'points': 190}, {'pk': 3, 'points': 150}, {'pk': 4, 'points': None}]>
Anyone has any idea of why this happens?
Thanks!
I should mention that I can fix that by annotating the queryset again and coalescing the null values to 0, like this:
everyone.\
annotate(real_points=Concat(Coalesce(F('points'), 0), Value(''))).\
values('pk', 'real_points')
<QuerySet [{'pk': 2, 'real_points': 190}, {'pk': 3, 'real_points': 150}, {'pk': 4, 'real_points': 0}]>
But I wish to understand why the union does not work as I expected in my original question.
EDIT:
I think I got it. A friend instructed me to use django-debug-toolbar to check my SQL queries to investigate further on this situation and I found out the following:
Since it's a union of two queries, the second query annotation is somehow not considered and the COALESCE to 0 is not used. By moving that to the first query it is propagated to the second query and I could achieve the expected result.
Basically, I changed the following:
# Moved the "Coalesce" to the initial query
people_with_points = Person.objects.\
filter(answer__correct=True).\
annotate(points=Coalesce(Sum('answer__points'), 0)).\
values('pk', 'points')
# Second query does not have it anymore
people_without_points = Person.objects.\
exclude(pk__in=people_with_points.values_list('pk')).\
values('pk', 'points')
# We will have the values with 0 here!
everyone = people_with_points | people_without_points
I asked this SO question yesterday.
The code I now have to display the choices list in my models.py is:
YOB_TYPES = Choices(*(
((0, 'select_yob', _(' Select Year of Birth')),
(2000, 'to_present', _('2000 to Present'))) +
tuple((i, str(i)) for i in xrange(1990, 2000)) +
((1, 'unspecified', _('Prefer not to answer')),))
)
....
year_of_birth_type = models.PositiveIntegerField(choices=YOB_TYPES, default=YOB_TYPES.select_yob, validators=[MinValueValidator(1)])
....
The choices list is now displayed with the year of birth running from 1990 to 1999 (ascending order) as shown below:
How do I change the code so that the year of birth dates are displayed 1999 to 1990 (decending order) as shown below:
I have searched but cannot locate anything related to my issue - reversing ( .reverse() ) the tuple output - maybe I am searching the wrong topic.
See this line:
tuple((i, str(i)) for i in xrange(1990, 2000)) +
Adjust like so:
tuple((i, str(i)) for i in xrange(1999, 1989, -1) +
The third argument specifies your "step", in this case -1 (to go in reverse). Remember that with xrange, the second parameter is not included in the iteration, so use 1989 rather than 1990 (the same reason why you used 2000 earlier, rather than 1999).
I am looking for fast method to count model's objects created within past 30 days, for each day separately. For example:
27.07.2013 (today) - 3 objects created
26.07.2013 - 0 objects created
25.07.2013 - 2 objects created
...
27.06.2013 - 1 objects created
I am going to use this data in google charts API. Have you any idea how to get this data efficiently?
items = Foo.objects.filter(createdate__lte=datetime.datetime.today(), createdate__gt=datetime.datetime.today()-datetime.timedelta(days=30)).\
values('createdate').annotate(count=Count('id'))
This will (1) filter results to contain the last 30 days, (2) select just the createdate field and (3) count the id's, grouping by all selected fields (i.e. createdate). This will return a list of dictionaries of the format:
[
{'createdate': <datetime.date object>, 'count': <int>},
{'createdate': <datetime.date object>, 'count': <int>},
...
]
EDIT:
I don't believe there's a way to get all dates, even those with count == 0, with just SQL. You'll have to insert each missing date through python code, e.g.:
import datetime
# needed to use .append() later on
items = list(items)
dates = [x.get('createdate') for x in items]
for d in (datetime.datetime.today() - datetime.timedelta(days=x) for x in range(0,30)):
if d not in dates:
items.append({'createdate': d, 'count': 0})
I think this can be somewhat more optimized solution with #knbk 's solution with python. This has fewer iterations and iterations inside SET is highly optimized in python (both in processing and in CPU-cycles).
from_date = datetime.date.today() - datetime.timedelta(days=7)
orders = Order.objects.filter(created_at=from_date, dealer__executive__branch__user=user)
orders = orders.annotate(count=Count('id')).values('created_at').order_by('created_at')
if len(orders) < 7:
orders_list = list(orders)
dates = set([(datetime.date.today() - datetime.timedelta(days=i)) for i in range(6)])
order_set = set([ord['created_at'] for ord in orders])
for dt in (order_set - dates):
orders_list.append({'created_at': dt, 'count': 0})
orders_list = sorted(orders_list, key=lambda item: item['created_at'])
else:
orders_list = orders
Imagine I have the following model:
class Person(models.Model):
...other stuff...
optional_first_name= models.CharField(max_length=50, blank=True)
How would I go about writing a request that returns an array of the most popular names, in decreasing order of occurence, with their counts, while ignoring the empty names?
i.e. for a database with 13 Leslies, 8 Andys, 3 Aprils, 1 Ron and 18 people who haven't specified their name, the output would be:
[('leslie', 13), ('andy', 8), ('april', 3), ('ron', 1)]
The closest I can get is by doing the following:
q= Person.objects.all()
q.query.group_by=['optional_first_name']
q.query.add_count_column()
q.values_list('optional_first_name', flat= True)
But it's still not quite what I want.
After some digging, finally found out:
Person.objects.values('optional_first_name').annotate(c=Count('optional_first_name')).order_by('-c')