Skip to content

Commit

Permalink
feat(stats): page de statistique des fiches pratiques (#796)
Browse files Browse the repository at this point in the history
## Description

🎸 Vue `DocumentStats` pour afficher le temps de lecture cumulé, le
nombre de visites, le nombre de notation et la moyenne de notations, de
toutes les fiches pratiques.

## Type de changement

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

### Points d'attention

🦺 les annotations de `ForumStats` et `ForumRating` sont associées via
une `SubQuery`

🦺 ajout du templatetag `convert_seconds_into_hours`
🦺 mise à jour de la factory `ForumStatFactory` pour déterminer les
valeurs de `visits`, `entry_visits` et `time_spent`
🦺 tri des items par le passage d'un param `sort` dans l'url

### Captures d'écran (optionnel)

page avec tri par défaut


![image](https://github.com/user-attachments/assets/edb499cd-aebb-4479-baad-65833bcfbc0a)

page avec tri sur le nombre de visites


![image](https://github.com/user-attachments/assets/72442619-77fe-4dd6-b67d-42344fa478c8)

bas de la page statistiques avec le lien vers la nouvelle vue


![image](https://github.com/user-attachments/assets/bc14e223-be76-46f9-ae05-aab7dee51da4)
  • Loading branch information
vincentporte authored Oct 9, 2024
1 parent c830f0a commit 41237f1
Show file tree
Hide file tree
Showing 9 changed files with 1,038 additions and 8 deletions.
4 changes: 3 additions & 1 deletion lacommunaute/stats/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ class Meta:
model = ForumStat

class Params:
for_snapshot = factory.Trait(period="week", date=datetime.date(2024, 5, 20))
for_snapshot = factory.Trait(
period="week", date=datetime.date(2024, 5, 20), visits=100, entry_visits=50, time_spent=23700
)
for_snapshot_older = factory.Trait(period="week", date=datetime.date(2024, 5, 13))
834 changes: 834 additions & 0 deletions lacommunaute/stats/tests/__snapshots__/tests_views.ambr

Large diffs are not rendered by default.

58 changes: 55 additions & 3 deletions lacommunaute/stats/tests/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from machina.core.loading import get_class
from pytest_django.asserts import assertContains, assertNotContains

from lacommunaute.forum.factories import ForumFactory, ForumRatingFactory
from lacommunaute.forum.factories import CategoryForumFactory, ForumFactory, ForumRatingFactory
from lacommunaute.stats.enums import Period
from lacommunaute.stats.factories import ForumStatFactory, StatFactory
from lacommunaute.surveys.factories import DSPFactory
Expand Down Expand Up @@ -167,11 +167,11 @@ def test_visitors_in_context_data(self, client, db, setup_statistiques_data, exp
assert response.status_code == 200
assert response.context["stats"] == expected

def test_link_to_weekly_lastest_stats_view(self, client, db):
def test_link_to_document_stats_view(self, client, db):
url = reverse("stats:statistiques")
response = client.get(url)
assert response.status_code == 200
assertContains(response, reverse("stats:redirect_to_latest_weekly_stats"))
assertContains(response, reverse("stats:document_stats"))


class TestMonthlyVisitorsView:
Expand Down Expand Up @@ -254,6 +254,58 @@ def test_navigation(self, client, db, snapshot):
assert str(parse_response_to_soup(response, selector=".c-breadcrumb")) == snapshot(name="breadcrumb")


@pytest.fixture(name="document_stats_setup")
def document_stats_setup_fixture(db):
category = CategoryForumFactory()
fa = ForumFactory(name="A", parent=category)
fb = ForumFactory(name="B", parent=category)
fc = ForumFactory(name="C", parent=category)
fd = ForumFactory(name="D", parent=category)
ForumStatFactory(forum=fa, period="week", visits=70, time_spent=40 * 60)
ForumStatFactory(forum=fb, period="week", visits=100, time_spent=30 * 60)
ForumStatFactory(forum=fc, period="week", visits=90, time_spent=20 * 60)
ForumStatFactory(forum=fd, period="week", visits=80, time_spent=10 * 60)
ForumRatingFactory.create_batch(2, forum=fa, rating=4)
ForumRatingFactory.create_batch(1, forum=fb, rating=3)
ForumRatingFactory.create_batch(4, forum=fc, rating=2)
ForumRatingFactory.create_batch(3, forum=fd, rating=5)

# undesired forum
ForumFactory(name="Forum not in Document area")
ForumFactory(name="Forum wo ForumStats", parent=category)

return category


class TestForumStatView:
@pytest.mark.parametrize(
"sort_key,snapshot_name",
[
(None, "sort_by_sum_time_spent"),
("sum_time_spent", "sort_by_sum_time_spent"),
("sum_visits", "sort_by_sum_visits"),
("avg_rating", "sort_by_avg_rating"),
("count_rating", "sort_by_count_rating"),
("unknown", "sort_by_sum_time_spent"),
],
)
def test_sort_key(self, client, db, document_stats_setup, sort_key, snapshot_name, snapshot):
url = reverse("stats:document_stats") + f"?sort={sort_key}" if sort_key else reverse("stats:document_stats")
response = client.get(url)
assert response.status_code == 200
assert str(
parse_response_to_soup(
response, selector="main", replace_in_href=[forum for forum in document_stats_setup.get_children()]
)
) == snapshot(name=snapshot_name)

def test_num_queries(self, client, db, document_stats_setup, django_assert_num_queries):
django_session_num_of_queries = 6
expected_queries_in_view = 1
with django_assert_num_queries(expected_queries_in_view + django_session_num_of_queries):
client.get(reverse("stats:document_stats"))


class TestForumStatWeekArchiveView:
def get_url_from_date(self, date):
return reverse(
Expand Down
2 changes: 2 additions & 0 deletions lacommunaute/stats/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from lacommunaute.stats.views import (
DailyDSPView,
DocumentStatsView,
ForumStatWeekArchiveView,
MonthlyVisitorsView,
StatistiquesPageView,
Expand All @@ -17,4 +18,5 @@
path("dsp/", DailyDSPView.as_view(), name="dsp"),
path("weekly/<int:year>/<int:week>/", ForumStatWeekArchiveView.as_view(), name="forum_stat_week_archive"),
path("weekly/", redirect_to_latest_weekly_stats, name="redirect_to_latest_weekly_stats"),
path("documents/", DocumentStatsView.as_view(), name="document_stats"),
]
48 changes: 46 additions & 2 deletions lacommunaute/stats/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
import logging

from dateutil.relativedelta import relativedelta
from django.db.models import Avg, CharField, Count, Q
from django.db.models import Avg, CharField, Count, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Cast
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.dateformat import format
from django.utils.timezone import localdate
from django.views import View
from django.views.generic.base import TemplateView
from django.views.generic.dates import WeekArchiveView

from lacommunaute.forum.models import Forum
from lacommunaute.forum.models import Forum, ForumRating
from lacommunaute.stats.models import ForumStat, Stat
from lacommunaute.surveys.models import DSP
from lacommunaute.utils.json import extract_values_in_list
Expand Down Expand Up @@ -164,6 +165,49 @@ def get_context_data(self, **kwargs):
return context


class DocumentStatsView(View):
def get_objects_with_stats_and_ratings(self):
objects = (
Forum.objects.filter(parent__type=Forum.FORUM_CAT, forumstat__period="week")
.annotate(sum_visits=Sum("forumstat__visits"))
.annotate(sum_time_spent=Sum("forumstat__time_spent"))
.select_related("parent", "partner")
.order_by("id")
)
ratings = ForumRating.objects.filter(forum=OuterRef("pk")).values("forum")
return objects.annotate(
avg_rating=Subquery(ratings.annotate(avg_rating=Avg("rating")).values("avg_rating")),
count_rating=Subquery(ratings.annotate(count_rating=Count("rating")).values("count_rating")),
)

def get_sort_fields(self):
return [
{"key": "sum_time_spent", "label": "Temps de lecture"},
{"key": "sum_visits", "label": "Nombre de Visites"},
{"key": "count_rating", "label": "Nombres de notations"},
{"key": "avg_rating", "label": "Moyenne des notations"},
]

def get(self, request, *args, **kwargs):
objects = self.get_objects_with_stats_and_ratings()
sort_key = (
request.GET.get("sort")
if request.GET.get("sort") in [field["key"] for field in self.get_sort_fields()]
else "sum_time_spent"
)
objects = objects.order_by("-" + sort_key)

return render(
request,
"stats/documents.html",
{
"objects": objects,
"sort_key": sort_key,
"sort_fields": self.get_sort_fields(),
},
)


def redirect_to_latest_weekly_stats(request):
latest_weekly_stat = ForumStat.objects.filter(period="week").order_by("-date").first()

Expand Down
78 changes: 78 additions & 0 deletions lacommunaute/templates/stats/documents.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{% extends "layouts/base.html" %}
{% load static %}
{% load i18n %}
{% load date_filters %}
{% load str_filters %}
{% block title %}Statistiques des Fiches Pratiques{{ block.super }}{% endblock %}
{% block body_class %}p-statistiques{{ block.super }}{% endblock %}
{% block breadcrumb %}
<div class="container">
<nav class="c-breadcrumb" aria-label="Fil d'ariane">
<ol class="breadcrumb">
<li class="breadcrumb-item">{% trans "Back to" %}</li>
<li class="breadcrumb-item">
<a href="{% url 'stats:statistiques' %}">Statistiques</a>
</li>
</ol>
</nav>
</div>
{% endblock %}
{% block content %}
<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">Statistiques des fiches pratiques</h1>
</div>
</div>
</div>
</section>
<section class="s-section">
<div class="s-section__container container">
<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">
<table class="table">
<caption>Sont présentes dans ce tableau, les fiches pratiques de l'espace Documents.
<br>
Le nombre et la moyenne des notations sont calculés en temps réel.
<br>
Le nombre de visites et le cumul du temps de lecture est calculé hebdomadairement, chaque lundi matin.
</caption>
{% with sort_fields=sort_fields %}
<thead>
<tr>
<th scope="col">Fiche Pratique</th>
{% for field in sort_fields %}
<th scope="col">
<a href="{{ request.path }}?sort={{ field.key }}" class="text-decoration-none">
{{ field.label }}
{% if sort_key == field.key %}<i class="ri-arrow-down-s-fill"></i>{% endif %}
</a>
</th>
{% endfor %}
</tr>
</thead>
{% endwith %}
<tbody>
{% for obj in objects %}
<tr>
<th scope="row">
<a href="{{ obj.absolute_url }}">{{ obj.name }}</a>
<br>
{% if obj.partner %}<span class="text-muted">en partenariat avec {{ obj.partner.name }}</span>{% endif %}
</th>
<td>{{ obj.sum_time_spent|convert_seconds_into_hours }}</td>
<td>{{ obj.sum_visits }}</td>
<td>{{ obj.count_rating|default_if_none:"pas de notation" }}</td>
<td>{{ obj.avg_rating|floatformat:2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
4 changes: 2 additions & 2 deletions lacommunaute/templates/stats/statistiques.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ <h2>{{ dsp_count }} Diagnostique{{ dsp_count|pluralizefr }} Parcours IAE</h2>
</div>
<div class="s-section__row row">
<div class="col-12 col-lg-auto">
<a href="{% url 'stats:redirect_to_latest_weekly_stats' %}" class="btn btn-outline-primary btn-ico btn-block">
Accès aux statistiques hebdomadaires
<a href="{% url 'stats:document_stats' %}" class="btn btn-outline-primary btn-ico btn-block">
Accès aux statistiques des fiches pratiques
</a>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions lacommunaute/utils/templatetags/date_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ def relativetimesince_fr(d):
return f"{date(d,'l')}, {time(d)}"

return f"il y a {timesince(d)}"


@register.filter(is_safe=True)
def convert_seconds_into_hours(value, default=None):
if value is None:
return "0h 00min"
hours = value // 3600
minutes = (value % 3600) // 60
return f"{hours}h {minutes:02d}min"
9 changes: 9 additions & 0 deletions lacommunaute/utils/tests/tests_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ def test_urlize(self):
self.assertEqual(urlize(img), img)


class TestUtilsTemplateTags:
@pytest.mark.parametrize(
"value,expected_result", [(900, "0h 15min"), (3600, "1h 00min"), (7320, "2h 02min"), (None, "0h 00min")]
)
def test_convert_seconds_into_hours(self, value, expected_result):
template = Template("{% load date_filters %}{{ value|convert_seconds_into_hours }}")
assert template.render(Context({"value": value})) == expected_result


class UtilsTemplateTagsTestCase(TestCase):
def test_pluralizefr(self):
"""Test `pluralizefr` template tag."""
Expand Down

0 comments on commit 41237f1

Please sign in to comment.