Skip to content

Commit

Permalink
feat(stats): page hebdo partenaires fiches pratiques (#695)
Browse files Browse the repository at this point in the history
## Description

🎸 Ajout d'une `WeekArchiveView` basée sur le modèle `ForumStat`
🎸 Ajout en contexte des stats de visiteurs à la période consultée
🎸 Ajout de la liste des `Forum` les plus notés (au moins 2 notes) sur la
période consultée

## Type de changement

🎢 Nouvelle fonctionnalité (changement non cassant qui ajoute une
fonctionnalité).

### Points d'attention

🦺 factorisation de la génération des stats des visiteurs : utilisés dans
2 vues
🦺 pagination vers les semaines existantes, pas d'accès aux semaines
futures
🦺 affichage des forums les plus vus, limités à 15

### Captures d'écran (optionnel)


![image](https://github.com/gip-inclusion/itou-communaute-django/assets/11419273/18de6014-0e16-4f2a-b88e-eb41582cb194)
  • Loading branch information
vincentporte authored Jul 1, 2024
1 parent cd0d301 commit f7f26b8
Show file tree
Hide file tree
Showing 9 changed files with 525 additions and 44 deletions.
6 changes: 6 additions & 0 deletions lacommunaute/forum/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ class ForumRatingFactory(factory.django.DjangoModelFactory):

class Meta:
model = ForumRating

@factory.post_generation
def set_created(self, create, extracted, **kwargs):
if extracted:
self.created = extracted
self.save()
3 changes: 3 additions & 0 deletions lacommunaute/forum/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def is_newsfeed(self):
def get_session_rating(self, session_key):
return getattr(ForumRating.objects.filter(forum=self, session_id=session_key).first(), "rating", None)

def get_average_rating(self):
return ForumRating.objects.filter(forum=self).aggregate(models.Avg("rating"))["rating__avg"]


class ForumRating(DatedModel):
session_id = models.CharField(max_length=40)
Expand Down
7 changes: 7 additions & 0 deletions lacommunaute/forum/tests/tests_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,10 @@ def test_get_session_rating(self):

ForumRatingFactory(forum=forum, session_id=forum_rating.session_id, rating=forum_rating.rating + 1)
self.assertEqual(forum.get_session_rating(forum_rating.session_id), forum_rating.rating + 1)

def test_get_average_rating(self):
forum = ForumFactory()
ForumRatingFactory(forum=forum, rating=1)
ForumRatingFactory(forum=forum, rating=5)

self.assertEqual(forum.get_average_rating(), 3)
3 changes: 3 additions & 0 deletions lacommunaute/stats/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ class ForumStatFactory(factory.django.DjangoModelFactory):

class Meta:
model = ForumStat

class Params:
for_snapshot = factory.Trait(period="week", date=datetime.date(2024, 5, 20))
96 changes: 96 additions & 0 deletions lacommunaute/stats/tests/__snapshots__/tests_views.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,102 @@
</nav>
'''
# ---
# name: TestForumStatWeekArchiveView.test_header_and_breadcrumb[breadcrumb]
'''
<nav aria-label="Fil d'ariane" class="c-breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">Retourner vers</li>
<li class="breadcrumb-item">
<a href="/statistiques/">Statistiques</a>
</li>
</ol>
</nav>
'''
# ---
# name: TestForumStatWeekArchiveView.test_header_and_breadcrumb[title-01]
'''
<section class="s-title-01 mt-lg-5">
<div class="s-title-01__container container">
<div class="s-title-01__row row">
<div class="s-title-01__col col-12">
<h1 class="s-title-01__title h1">
<strong>Partenariat Academie France Travail x La communauté de l'inclusion</strong>
<br/>
Statistiques de la semaine du 20 mai 2024 au 26 mai 2024
</h1>
</div>
</div>
</div>
</section>
'''
# ---
# name: TestForumStatWeekArchiveView.test_most_rated_forums[most_rated_forums]
'''
<div class="s-section__row row" id="most_rated">
<div class="s-section__col col-12">
<div class="c-box mb-3 mb-md-5">
<h2>Les 1 fiches pratiques les plus notées sur la période</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Fiche Pratique</th>
<th scope="col">Nombres de notations de la semaine</th>
<th scope="col">Notation moyenne totale</th>
</tr>
</thead>
<tbody>

<tr>
<th scope="row">Forum A</th>
<td>2</td>
<td>3,00</td>
</tr>

</tbody>
</table>
</div>
</div>
</div>
'''
# ---
# name: TestForumStatWeekArchiveView.test_most_viewed_forums[most_viewed_forums]
'''
<div class="s-section__row row" id="most_viewed">
<div class="s-section__col col-12">
<div class="c-box mb-3 mb-md-5">
<h2>Les 2 fiches pratiques les plus lues sur la période</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Fiche Pratique</th>
<th scope="col">Visiteurs uniques</th>
<th scope="col">Visiteurs uniques entrants</th>
<th scope="col">Temps de lecture total</th>
</tr>
</thead>
<tbody>

<tr>
<th scope="row">Forum B</th>
<td>17</td>
<td>5</td>
<td>1978 secondes</td>
</tr>

<tr>
<th scope="row">Forum A</th>
<td>10</td>
<td>8</td>
<td>1000 secondes</td>
</tr>

</tbody>
</table>
</div>
</div>
</div>
'''
# ---
# name: TestMonthlyVisitorsView.test_navigation[breadcrumb]
'''
<nav aria-label="Fil d'ariane" class="c-breadcrumb">
Expand Down
186 changes: 161 additions & 25 deletions lacommunaute/stats/tests/tests_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from datetime import date

import pytest # noqa
from dateutil.relativedelta import relativedelta
from django.test import TestCase
from django.urls import reverse
Expand All @@ -6,10 +9,11 @@
from django.utils.timezone import localdate
from faker import Faker
from machina.core.loading import get_class
from pytest_django.asserts import assertContains
from pytest_django.asserts import assertContains, assertNotContains

from lacommunaute.forum.factories import ForumFactory, ForumRatingFactory
from lacommunaute.stats.enums import Period
from lacommunaute.stats.factories import StatFactory
from lacommunaute.stats.factories import ForumStatFactory, StatFactory
from lacommunaute.surveys.factories import DSPFactory
from lacommunaute.utils.math import percent
from lacommunaute.utils.testing import parse_response_to_soup
Expand All @@ -22,33 +26,10 @@
class StatistiquesPageTest(TestCase):
def test_context_data(self):
url = reverse("stats:statistiques")
date = timezone.now()
names = ["nb_uniq_engaged_visitors", "nb_uniq_visitors", "nb_uniq_active_visitors"]
for name in names:
StatFactory(name=name, date=date)
undesired_period_stat = StatFactory(
period=Period.WEEK, date=date - timezone.timedelta(days=7), name="nb_uniq_engaged_visitors"
)
undesired_date_stat = StatFactory(
period=Period.DAY, date=date - timezone.timedelta(days=91), name="nb_uniq_engaged_visitors"
)

response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "stats/statistiques.html")

# expected values
self.assertIn("stats", response.context)
self.assertIn("date", response.context["stats"])
self.assertIn("nb_uniq_engaged_visitors", response.context["stats"])
self.assertIn("nb_uniq_visitors", response.context["stats"])
self.assertIn("nb_uniq_active_visitors", response.context["stats"])
self.assertEqual(response.context["stats"]["date"][0], date.strftime("%Y-%m-%d"))

# undesired values
self.assertNotIn(undesired_period_stat.date.strftime("%Y-%m-%d"), response.context["stats"]["date"])
self.assertNotIn(undesired_date_stat.date.strftime("%Y-%m-%d"), response.context["stats"]["date"])

def test_month_datas_in_context(self):
today = localdate()
url = reverse("stats:statistiques")
Expand Down Expand Up @@ -121,6 +102,33 @@ def test_navigation(self):
self.assertContains(response, f"<a href={reverse('stats:monthly_visitors')}>")


@pytest.fixture(name="setup_statistiques_data")
def setup_statistiques_data_fixture(request):
last_visible_date = date(2024, 6, 30)
first_visible_date = last_visible_date - timezone.timedelta(days=89)
if request.param == "undesired_data_setup":
return [
StatFactory(name="nb_uniq_visitors", date=first_visible_date, period=Period.MONTH),
StatFactory(name="nb_uniq_visitors", date=first_visible_date, period=Period.WEEK),
StatFactory(name=faker.word(), date=first_visible_date, period=Period.DAY),
StatFactory(
name="nb_uniq_visitors", date=first_visible_date - timezone.timedelta(days=1), period=Period.DAY
),
StatFactory(
name="nb_uniq_visitors", date=last_visible_date + timezone.timedelta(days=1), period=Period.DAY
),
]
if request.param == "desired_data_setup":
stats = [
("nb_uniq_visitors", first_visible_date, 8469),
("nb_uniq_visitors", last_visible_date, 8506),
("nb_uniq_engaged_visitors", first_visible_date, 128),
("nb_uniq_engaged_visitors", last_visible_date, 5040),
]
return [StatFactory(name=name, date=date, value=value) for name, date, value in stats]
return None


class TestStatistiquesPageView:
def test_dsp_count(self, client, db, snapshot):
DSPFactory.create_batch(10)
Expand All @@ -129,6 +137,34 @@ def test_dsp_count(self, client, db, snapshot):
assert response.status_code == 200
assert str(parse_response_to_soup(response, selector="#daily_dsp")) == snapshot(name="dsp")

@pytest.mark.parametrize(
"setup_statistiques_data,expected",
[
(
None,
{"date": [], "nb_uniq_visitors": [], "nb_uniq_engaged_visitors": []},
),
(
"undesired_data_setup",
{"date": [], "nb_uniq_visitors": [], "nb_uniq_engaged_visitors": []},
),
(
"desired_data_setup",
{
"date": ["2024-04-02", "2024-06-30"],
"nb_uniq_visitors": [8469, 8506],
"nb_uniq_engaged_visitors": [128, 5040],
},
),
],
indirect=["setup_statistiques_data"],
)
def test_visitors_in_context_data(self, client, db, setup_statistiques_data, expected):
url = reverse("stats:statistiques")
response = client.get(url)
assert response.status_code == 200
assert response.context["stats"] == expected


class TestMonthlyVisitorsView:
def test_context_data(self, client, db):
Expand Down Expand Up @@ -208,3 +244,103 @@ def test_navigation(self, client, db, snapshot):
response = client.get(url)
assert response.status_code == 200
assert str(parse_response_to_soup(response, selector=".c-breadcrumb")) == snapshot(name="breadcrumb")


class TestForumStatWeekArchiveView:
def get_url_from_date(self, date):
return reverse(
"stats:forum_stat_week_archive", kwargs={"year": date.strftime("%Y"), "week": date.strftime("%W")}
)

def test_header_and_breadcrumb(self, client, db, snapshot):
response = client.get(self.get_url_from_date(ForumStatFactory(for_snapshot=True).date))
assert response.status_code == 200
assert str(parse_response_to_soup(response, selector=".s-title-01")) == snapshot(name="title-01")
assert str(parse_response_to_soup(response, selector=".c-breadcrumb")) == snapshot(name="breadcrumb")

def test_navigation(self, client, db):
weeks = [date.today() - relativedelta(weeks=i) for i in range(15, 10, -1)]
for week in weeks[1:4]:
ForumStatFactory(date=week, for_snapshot=True)

test_cases = [
{"test_week": weeks[1], "not_contains": [weeks[0]], "contains": [weeks[2]]},
{"test_week": weeks[2], "not_contains": [], "contains": [weeks[1], weeks[3]]},
{"test_week": weeks[3], "not_contains": [weeks[4]], "contains": [weeks[2]]},
]

for test_case in test_cases:
response = client.get(self.get_url_from_date(test_case["test_week"]))
for week in test_case["not_contains"]:
assertNotContains(response, self.get_url_from_date(week))
for week in test_case["contains"]:
assertContains(response, self.get_url_from_date(week))

# out of bound
for week in [weeks[0], weeks[4]]:
response = client.get(self.get_url_from_date(week))
assert response.status_code == 404

def test_most_viewed_forums(self, client, db, snapshot):
forums_stats = [
ForumStatFactory(for_snapshot=True, visits=10, entry_visits=8, time_spent=1000, forum__name="Forum A"),
ForumStatFactory(for_snapshot=True, visits=17, entry_visits=5, time_spent=1978, forum__name="Forum B"),
]

response = client.get(self.get_url_from_date(forums_stats[0].date))
assert response.status_code == 200
assert str(
parse_response_to_soup(
response, selector="#most_viewed", replace_in_href=[fs.forum for fs in forums_stats]
)
) == snapshot(name="most_viewed_forums")

def test_paginated_most_viewed_forums(self, client, db):
ForumStatFactory.create_batch(16, for_snapshot=True)
response = client.get(reverse("stats:forum_stat_week_archive", kwargs={"year": 2024, "week": 21}))
assert response.status_code == 200
assert len(response.context_data["forum_stats"]) == 15

def test_most_rated_forums(self, client, db, snapshot):
fs = ForumStatFactory(for_snapshot=True, forum__name="Forum A")

# rating within range
ForumRatingFactory.create_batch(2, rating=5, forum=fs.forum, set_created=fs.date)
# rating out of range
ForumRatingFactory.create_batch(2, rating=1, forum=fs.forum)

# undesired forum
ForumFactory()

# undesired rating
ForumRatingFactory(rating=4)

response = client.get(self.get_url_from_date(fs.date))
assert response.status_code == 200
assert str(parse_response_to_soup(response, selector="#most_rated")) == snapshot(name="most_rated_forums")

def test_visitors(self, client, db, snapshot):
fs = ForumStatFactory(for_snapshot=True)

# relevant
relevant_dates = [
fs.date,
fs.date + relativedelta(days=6),
fs.date + relativedelta(days=6) - relativedelta(days=89),
]
visitors = [10, 11, 12]
for stat_date, visitor_count in zip(relevant_dates, visitors):
StatFactory(date=stat_date, name="nb_uniq_visitors", value=visitor_count)

# undesired
for stat_date in [fs.date + relativedelta(weeks=1), fs.date + relativedelta(days=6) - relativedelta(days=90)]:
StatFactory(date=stat_date, name="nb_uniq_visitors", value=99)

response = client.get(self.get_url_from_date(fs.date))
assert response.status_code == 200
expected_stats = {
"date": ["2024-02-27", "2024-05-20", "2024-05-26"],
"nb_uniq_visitors": [12, 10, 11],
"nb_uniq_engaged_visitors": [],
}
assert response.context_data["stats"] == expected_stats
3 changes: 2 additions & 1 deletion lacommunaute/stats/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import path

from lacommunaute.stats.views import DailyDSPView, MonthlyVisitorsView, StatistiquesPageView
from lacommunaute.stats.views import DailyDSPView, ForumStatWeekArchiveView, MonthlyVisitorsView, StatistiquesPageView


app_name = "stats"
Expand All @@ -9,4 +9,5 @@
path("", StatistiquesPageView.as_view(), name="statistiques"),
path("monthly-visitors/", MonthlyVisitorsView.as_view(), name="monthly_visitors"),
path("dsp/", DailyDSPView.as_view(), name="dsp"),
path("aft/<int:year>/<int:week>/", ForumStatWeekArchiveView.as_view(), name="forum_stat_week_archive"),
]
Loading

0 comments on commit f7f26b8

Please sign in to comment.