Skip to content

Commit 85cd5f4

Browse files
committed
Re-add analytics view.
1 parent 6eea826 commit 85cd5f4

File tree

4 files changed

+160
-2
lines changed

4 files changed

+160
-2
lines changed

project/newsletter/test_views.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,81 @@ def test_update(self):
419419
)
420420
self.assertRedirects(response, reverse("newsletter:list_posts"))
421421
self.assertEqual(self.subscription.categories.get(), self.data.career)
422+
423+
424+
class TestAnalytics(DataTestCase):
425+
def test_basic(self):
426+
self.client.force_login(self.user)
427+
response = self.client.get(reverse("newsletter:analytics"))
428+
self.assertTemplateUsed(response, "staff/analytics.html")
429+
self.assertEqual(
430+
response.context["aggregates"],
431+
{
432+
"Subscriptions": 2,
433+
"Subscriptions (30 days)": 2,
434+
"Subscriptions (90 days)": 2,
435+
"Subscriptions (180 days)": 2,
436+
"Posts": 3,
437+
"Posts (30 days)": 3,
438+
"Posts (90 days)": 3,
439+
"Posts (180 days)": 3,
440+
},
441+
)
442+
443+
self.assertEqual(
444+
response.context["subscription_category_aggregates"],
445+
{
446+
self.data.career.title: 1,
447+
self.data.social.title: 1,
448+
},
449+
)
450+
self.assertEqual(
451+
response.context["post_category_aggregates"],
452+
{
453+
self.data.career.title: 2,
454+
self.data.social.title: 2,
455+
},
456+
)
457+
458+
def test_date_aggregates(self):
459+
# Create users that are outside the cut-off points.
460+
for days in [31, 91, 181]:
461+
user = User.objects.create_user(username=f"days{days}")
462+
subscription = Subscription.objects.create(user=user)
463+
# We can't specify created in .create() because it's automatically set.
464+
Subscription.objects.filter(id=subscription.id).update(
465+
created=timezone.now() - timedelta(days=days)
466+
)
467+
subscription.categories.set([self.data.career, self.data.social])
468+
469+
self.client.force_login(self.user)
470+
response = self.client.get(reverse("newsletter:analytics"))
471+
self.assertTemplateUsed(response, "staff/analytics.html")
472+
self.assertEqual(
473+
response.context["aggregates"],
474+
{
475+
"Subscriptions": 8,
476+
"Subscriptions (30 days)": 2,
477+
"Subscriptions (90 days)": 4,
478+
"Subscriptions (180 days)": 6,
479+
"Posts": 3,
480+
"Posts (30 days)": 3,
481+
"Posts (90 days)": 3,
482+
"Posts (180 days)": 3,
483+
},
484+
)
485+
486+
self.assertEqual(
487+
response.context["subscription_category_aggregates"],
488+
{
489+
self.data.career.title: 4,
490+
self.data.social.title: 4,
491+
},
492+
)
493+
self.assertEqual(
494+
response.context["post_category_aggregates"],
495+
{
496+
self.data.career.title: 2,
497+
self.data.social.title: 2,
498+
},
499+
)

project/newsletter/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
path("markdown/uploader/", views.markdown_uploader, name="markdown_uploader"),
88
path("", views.landing, name="landing"),
99
path("account/", views.update_subscription, name="update_subscription"),
10+
path("analytics/", views.analytics, name="analytics"),
1011
path("post/unpublished/", views.unpublished_posts, name="unpublished_posts"),
1112
path("post/create/", views.create_post, name="create_post"),
1213
path("post/<slug>/update/", views.update_post, name="update_post"),

project/newsletter/views.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import uuid
3+
from datetime import timedelta
34

45
from django.conf import settings
56
from django.contrib import messages
@@ -9,16 +10,17 @@
910
from django.core.files.base import ContentFile
1011
from django.core.files.storage import default_storage
1112
from django.core.paginator import Paginator
12-
from django.db.models import Case, F, Value, When
13+
from django.db.models import Case, F, Value, When, Count, Q
1314
from django.http import Http404, HttpResponse, JsonResponse
1415
from django.shortcuts import get_object_or_404, redirect, render
16+
from django.utils import timezone
1517
from django.utils.translation import gettext_lazy as _
1618
from django.views.decorators.http import require_http_methods
1719
from martor.utils import LazyEncoder
1820

1921
from project.newsletter import operations
2022
from project.newsletter.forms import PostForm, SubscriptionForm
21-
from project.newsletter.models import Post, Subscription
23+
from project.newsletter.models import Category, Post, Subscription
2224

2325
LIST_POSTS_PAGE_SIZE = 100
2426

@@ -162,6 +164,80 @@ def update_post(request, slug):
162164
return render(request, "staff/post_form.html", {"form": form, "post": post})
163165

164166

167+
@staff_member_required(login_url=settings.LOGIN_URL)
168+
@require_http_methods(["GET"])
169+
def analytics(request):
170+
"""
171+
The post detail view.
172+
"""
173+
now = timezone.now()
174+
subscription_aggregates = Subscription.objects.all().aggregate(
175+
subscriptions=Count("user", filter=Q(categories__isnull=False)),
176+
subscriptions_30_days=Count(
177+
"id",
178+
filter=Q(categories__isnull=False, created__gte=now - timedelta(days=30)),
179+
),
180+
subscriptions_90_days=Count(
181+
"id",
182+
filter=Q(categories__isnull=False, created__gte=now - timedelta(days=90)),
183+
),
184+
subscriptions_180_days=Count(
185+
"id",
186+
filter=Q(categories__isnull=False, created__gte=now - timedelta(days=180)),
187+
),
188+
)
189+
subscription_category_aggregates = dict(
190+
Category.objects.annotate(count=Count("subscriptions"))
191+
.order_by("title")
192+
.values_list("title", "count")
193+
)
194+
post_aggregates = Post.objects.all().aggregate(
195+
posts=Count("id"),
196+
posts_30_days=Count(
197+
"id",
198+
filter=Q(created__gte=now - timedelta(days=30)),
199+
),
200+
posts_90_days=Count(
201+
"id",
202+
filter=Q(created__gte=now - timedelta(days=90)),
203+
),
204+
posts_180_days=Count(
205+
"id",
206+
filter=Q(created__gte=now - timedelta(days=180)),
207+
),
208+
)
209+
post_category_aggregates = dict(
210+
Category.objects.annotate(count=Count("posts"))
211+
.order_by("title")
212+
.values_list("title", "count")
213+
)
214+
215+
return render(
216+
request,
217+
"staff/analytics.html",
218+
{
219+
"aggregates": {
220+
"Subscriptions": subscription_aggregates["subscriptions"],
221+
"Subscriptions (30 days)": subscription_aggregates[
222+
"subscriptions_30_days"
223+
],
224+
"Subscriptions (90 days)": subscription_aggregates[
225+
"subscriptions_90_days"
226+
],
227+
"Subscriptions (180 days)": subscription_aggregates[
228+
"subscriptions_180_days"
229+
],
230+
"Posts": post_aggregates["posts"],
231+
"Posts (30 days)": post_aggregates["posts_30_days"],
232+
"Posts (90 days)": post_aggregates["posts_90_days"],
233+
"Posts (180 days)": post_aggregates["posts_180_days"],
234+
},
235+
"subscription_category_aggregates": subscription_category_aggregates,
236+
"post_category_aggregates": post_category_aggregates,
237+
},
238+
)
239+
240+
165241
@staff_member_required(login_url=settings.LOGIN_URL)
166242
@require_http_methods(["POST"])
167243
def toggle_post_privacy(request, slug):

project/templates/base.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<a href="{% url "auth_login" %}" class="header item">Login</a>
3636
{% else %}
3737
<a href="{% url "newsletter:update_subscription" %}" class="header item">Settings</a>
38+
{% if request.user.is_staff %}
39+
<a href="{% url "newsletter:analytics" %}" class="header item">Analytics</a>
40+
{% endif %}
3841
{% endif %}
3942
<div class="right menu">
4043
<a class="item" href="https://github.com/tim-schilling/debug-tutorial" target="_blank">

0 commit comments

Comments
 (0)