1- """Aggregations feeding the admin dashboard analytics panels .
1+ """Aggregations feeding the dedicated Analytics admin page .
22
3- Exposes ``add_dashboard_metrics (context)``, called by the core dashboard
4- callback. Kept here so all page-view logic lives in the analytics app.
3+ Exposes ``full_metrics (context)``, called by the analytics view. Kept here so
4+ all page-view logic lives in the analytics app.
55"""
66
77from datetime import timedelta
1616from apps .analytics .models import PageView
1717
1818
19+ def _pct_change (current , previous ):
20+ """Signed % change vs the previous equal-length window."""
21+ if not previous :
22+ return None
23+ return round ((current - previous ) / previous * 100 )
24+
25+
1926def _daily_series (views , days = 30 ):
2027 """Zero-filled daily counts (oldest -> newest) with bar heights as a
2128 percentage of the busiest day, for the sparkline / bar chart."""
@@ -37,6 +44,24 @@ def _daily_series(views, days=30):
3744 return series , peak
3845
3946
47+ def _realtime_series (views , minutes = 60 , bucket = 5 ):
48+ """Counts grouped into bucket -minute slots over the last minutes."""
49+ now = timezone .now ()
50+ cur = now .replace (second = 0 , microsecond = 0 )
51+ start = cur - timedelta (minutes = minutes - 1 )
52+ nslots = - (- minutes // bucket ) # ceil
53+ slots = [0 ] * nslots
54+ for ts in views .filter (created_at__gte = start ).values_list ("created_at" , flat = True ):
55+ mi = int ((ts - start ).total_seconds () // 60 )
56+ if 0 <= mi < minutes :
57+ slots [mi // bucket ] += 1
58+ series = [{"min" : start + timedelta (minutes = i * bucket ), "span" : bucket , "count" : c } for i , c in enumerate (slots )]
59+ peak = max (slots , default = 0 ) or 1
60+ for s in series :
61+ s ["pct" ] = max (round (s ["count" ] / peak * 100 ), 3 ) if s ["count" ] else 0
62+ return series , peak
63+
64+
4065def _top_content (views , since , limit = 10 ):
4166 """Most-viewed content objects (resolved to title + admin/site links)."""
4267 rows = (
@@ -60,47 +85,50 @@ def _top_content(views, since, limit=10):
6085 return items
6186
6287
63- def add_dashboard_metrics (context ):
64- """Compact analytics for the admin dashboard landing page."""
65- now = timezone .now ()
66- d7 = now - timedelta (days = 7 )
67- d30 = now - timedelta (days = 30 )
68- views = PageView .objects .all ()
69-
70- context ["metrics" ] = {
71- "views_7d" : views .filter (created_at__gte = d7 ).count (),
72- "views_30d" : views .filter (created_at__gte = d30 ).count (),
73- "uniques_7d" : views .filter (created_at__gte = d7 ).values ("visitor_hash" ).distinct ().count (),
74- }
75- context ["top_pages" ] = list (
76- views .filter (created_at__gte = d30 ).values ("path" ).annotate (n = Count ("id" )).order_by ("-n" )[:8 ]
77- )
78- context ["views_series" ], context ["views_peak" ] = _daily_series (views , days = 30 )
79-
80-
8188def full_metrics (context ):
8289 """Richer analytics for the dedicated Analytics admin page."""
8390 now = timezone .now ()
8491 d7 = now - timedelta (days = 7 )
92+ d14 = now - timedelta (days = 14 )
8593 d30 = now - timedelta (days = 30 )
94+ d60 = now - timedelta (days = 60 )
8695 views = PageView .objects .all ()
8796
97+ v7 = views .filter (created_at__gte = d7 ).count ()
98+ v7_prev = views .filter (created_at__gte = d14 , created_at__lt = d7 ).count ()
99+ v30 = views .filter (created_at__gte = d30 ).count ()
100+ v30_prev = views .filter (created_at__gte = d60 , created_at__lt = d30 ).count ()
101+ u7 = views .filter (created_at__gte = d7 ).values ("visitor_hash" ).distinct ().count ()
102+ u7_prev = views .filter (created_at__gte = d14 , created_at__lt = d7 ).values ("visitor_hash" ).distinct ().count ()
103+ u30 = views .filter (created_at__gte = d30 ).values ("visitor_hash" ).distinct ().count ()
104+ u30_prev = views .filter (created_at__gte = d60 , created_at__lt = d30 ).values ("visitor_hash" ).distinct ().count ()
105+
88106 metrics = {
89- "views_7d" : views . filter ( created_at__gte = d7 ). count () ,
90- "views_30d" : views . filter ( created_at__gte = d30 ). count () ,
107+ "views_7d" : v7 ,
108+ "views_30d" : v30 ,
91109 "views_all" : views .count (),
92- "uniques_7d" : views . filter ( created_at__gte = d7 ). values ( "visitor_hash" ). distinct (). count () ,
93- "uniques_30d" : views . filter ( created_at__gte = d30 ). values ( "visitor_hash" ). distinct (). count () ,
110+ "uniques_7d" : u7 ,
111+ "uniques_30d" : u30 ,
94112 }
95113 context ["metrics" ] = metrics
114+ # (label, value, delta%) — delta is None for the all-time card (no baseline).
96115 context ["cards" ] = [
97- (_ ("Views (7 days)" ), metrics [ "views_7d" ] ),
98- (_ ("Views (30 days)" ), metrics [ "views_30d" ] ),
99- (_ ("Views (all time)" ), metrics ["views_all" ]),
100- (_ ("Unique visitors (7 days)" ), metrics [ "uniques_7d" ] ),
101- (_ ("Unique visitors (30 days)" ), metrics [ "uniques_30d" ] ),
116+ (_ ("Views (7 days)" ), v7 , _pct_change ( v7 , v7_prev ) ),
117+ (_ ("Views (30 days)" ), v30 , _pct_change ( v30 , v30_prev ) ),
118+ (_ ("Views (all time)" ), metrics ["views_all" ], None ),
119+ (_ ("Unique visitors (7 days)" ), u7 , _pct_change ( u7 , u7_prev ) ),
120+ (_ ("Unique visitors (30 days)" ), u30 , _pct_change ( u30 , u30_prev ) ),
102121 ]
103122 context ["views_series" ], context ["views_peak" ] = _daily_series (views , days = 30 )
123+
124+ rt = views .filter (created_at__gte = now - timedelta (hours = 1 ))
125+ context ["realtime" ] = {
126+ "views_1h" : rt .count (),
127+ "uniques_1h" : rt .values ("visitor_hash" ).distinct ().count (),
128+ }
129+ context ["realtime_series" ], context ["realtime_peak" ] = _realtime_series (views , minutes = 60 , bucket = 5 )
130+ context ["realtime_pages" ] = list (rt .values ("path" ).annotate (n = Count ("id" )).order_by ("-n" )[:5 ])
131+
104132 context ["top_content" ] = _top_content (views , d30 , limit = 10 )
105133 context ["top_pages" ] = list (
106134 views .filter (created_at__gte = d30 ).values ("path" ).annotate (n = Count ("id" )).order_by ("-n" )[:15 ]
@@ -112,3 +140,10 @@ def full_metrics(context):
112140 .annotate (n = Count ("id" ))
113141 .order_by ("-n" )[:10 ]
114142 )
143+ context ["top_campaigns" ] = list (
144+ views .filter (created_at__gte = d30 )
145+ .exclude (utm_source = "" )
146+ .values ("utm_source" , "utm_medium" , "utm_campaign" )
147+ .annotate (n = Count ("id" ))
148+ .order_by ("-n" )[:10 ]
149+ )
0 commit comments