Ajout d'une page de statistiques

Fix #322
This commit is contained in:
Jean-Marie Favreau 2025-02-23 01:20:41 +01:00
parent 7082a10805
commit d843eb209d
40 changed files with 63081 additions and 18 deletions

View File

@ -0,0 +1 @@
next_month

View File

@ -0,0 +1 @@
.ch-container{display:block}.ch-container,.ch-domain,.ch-domain-container,.ch-domain-container-animation-wrapper{overflow:visible}.ch-domain-container.in-transition .ch-domain-container-animation-wrapper{overflow:hidden}.ch-domain-bg{fill:transparent}.ch-domain-text{fill:currentColor;font-size:10px}.ch-subdomain{overflow:visible}.ch-subdomain-bg{fill:#ededed}.ch-subdomain-bg.highlight{stroke:#444;stroke-width:1px}.ch-subdomain-bg:hover{stroke:#000;stroke-width:1px}.ch-subdomain-text{font-size:8px;pointer-events:none}[data-theme=dark] .ch-subdomain-bg{fill:#2d333b}[data-theme=dark] .ch-subdomain-bg.highlight{stroke:#768390}[data-theme=dark] .ch-subdomain-bg:hover{stroke:#636e7b}#ch-plugin-legend>svg{background:transparent;color:currentColor}#ch-tooltip{background:#222;border-radius:2px;box-shadow:2px 2px 2px rgba(0,0,0,.2);box-sizing:border-box;color:#bbb;display:none;font-size:12px;line-height:1.4;padding:5px 10px;text-align:center}#ch-tooltip[data-show]{display:block}#ch-tooltip-arrow,#ch-tooltip-arrow:before{background:inherit;height:8px;position:absolute;width:8px}#ch-tooltip-arrow{visibility:hidden}#ch-tooltip-arrow:before{content:"";transform:rotate(45deg);visibility:visible}#ch-tooltip[data-popper-placement^=top]>#ch-tooltip-arrow{bottom:-4px;margin-left:-4px}#ch-tooltip[data-popper-placement^=bottom]>#ch-tooltip-arrow{margin-left:-4px;top:-4px}#ch-tooltip[data-popper-placement^=left]>#ch-tooltip-arrow{right:-4px}#ch-tooltip[data-popper-placement^=right]>#ch-tooltip-arrow{left:-4px}#ch-tooltip[data-theme=dark]{background:#636e7b;color:#cdd9e5}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.ch-container{display:block}.ch-container,.ch-domain,.ch-domain-container,.ch-domain-container-animation-wrapper{overflow:visible}.ch-domain-container.in-transition .ch-domain-container-animation-wrapper{overflow:hidden}.ch-domain-bg{fill:transparent}.ch-domain-text{fill:currentColor;font-size:10px}.ch-subdomain{overflow:visible}.ch-subdomain-bg{fill:#ededed}.ch-subdomain-bg.highlight{stroke:#444;stroke-width:1px}.ch-subdomain-bg:hover{stroke:#000;stroke-width:1px}.ch-subdomain-text{font-size:8px;pointer-events:none}[data-theme=dark] .ch-subdomain-bg{fill:#2d333b}[data-theme=dark] .ch-subdomain-bg.highlight{stroke:#768390}[data-theme=dark] .ch-subdomain-bg:hover{stroke:#636e7b}#ch-plugin-legend>svg{background:transparent;color:currentColor}#ch-tooltip{background:#222;border-radius:2px;box-shadow:2px 2px 2px rgba(0,0,0,.2);box-sizing:border-box;color:#bbb;display:none;font-size:12px;line-height:1.4;padding:5px 10px;text-align:center}#ch-tooltip[data-show]{display:block}#ch-tooltip-arrow,#ch-tooltip-arrow:before{background:inherit;height:8px;position:absolute;width:8px}#ch-tooltip-arrow{visibility:hidden}#ch-tooltip-arrow:before{content:"";transform:rotate(45deg);visibility:visible}#ch-tooltip[data-popper-placement^=top]>#ch-tooltip-arrow{bottom:-4px;margin-left:-4px}#ch-tooltip[data-popper-placement^=bottom]>#ch-tooltip-arrow{margin-left:-4px;top:-4px}#ch-tooltip[data-popper-placement^=left]>#ch-tooltip-arrow{right:-4px}#ch-tooltip[data-popper-placement^=right]>#ch-tooltip-arrow{left:-4px}#ch-tooltip[data-theme=dark]{background:#636e7b;color:#cdd9e5}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1906,3 +1906,7 @@ dialog {
.single-event header .buttons [role="button"] {
font-size: 90%;
}
#cal-heatmap-startday g {
cursor: pointer;
}

View File

@ -112,26 +112,18 @@
<footer class="container-fluid">
<div class="grid">
<div>
<a href="{% url 'mentions_legales' %}">{% picto_from_name "align-left" %} mentions légales</a>
</div>
<div>
<a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a>
</div>
<div>
<a href="{% url 'view_organisations' %}">{% picto_from_name "users" %} organisateurs</a>
</div>
<div>
<a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a>
<a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a><br>
<a href="{% url 'view_organisations' %}">{% picto_from_name "users" %} organisateurs</a><br>
<a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a><br>
<a href="{% url 'statistics' %}">{% picto_from_name "pie-chart" %} statistiques</a>
</div>
<div>
<a href="{% url 'about' %}">{% picto_from_name "users" %} à propos</a>
</div>
<div>
<a href="{% url 'contact' %}">{% picto_from_name "mail" %} contact</a>
</div>
<div>
<a href="{% url 'about' %}">{% picto_from_name "users" %} à propos</a><br>
<a href="{% url 'mentions_legales' %}">{% picto_from_name "align-left" %} mentions légales</a><br>
<a href="{% url 'contact' %}">{% picto_from_name "mail" %} contact</a><br>
{% if user.is_authenticated %}
<a href="{% url 'administration' %}">{% picto_from_name "settings" %} administrer</a>
<p>Vous êtes connecté(e) en tant que {{ user }}</p>

View File

@ -0,0 +1,127 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load static %}
{% block title %}{% block og_title %}Statistiques{% endblock %}{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
<script src="{% static 'js/d3.v7.min.js' %}"></script>
<script src="{% static 'cal-heatmap/cal-heatmap.min.js' %}"></script>
<script src="{% static 'cal-heatmap/plugins/CalendarLabel.min.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'cal-heatmap/plugins/Tooltip.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'cal-heatmap/cal-heatmap.css' %}">
{% endblock %}
{% block content %}
<article>
<header><h1>Statistiques</h1></header>
<p>On retrouve sur cette page une synthèse des événements publiés sur l'agenda culturel pommes de lune.</p>
<h2>Par date de l'événement</h2>
<p>Pour chaque date, on retrouve le nombre d'événements qui débutent à cette date (sauf événements récurrents).</p>
<div class="large-table">
<div id="cal-heatmap-startday"></div>
</div>
<h2>Par jour de création sur l'agenda</h2>
<p>Pour chaque date, on retrouve le nombre d'événements qui ont été créé ce jour-là, par import automatique ou par création manuelle.
Ce nombre ne tient pas en compte les événements qui sont des doublons d'événements déjà importés.
</p>
<div class="large-table">
<div id="cal-heatmap-creation"></div>
</div>
<h2>Par ville</h2>
<p>Nombre d'événements référencés par ville.</p>
<ul>
{% for v in nb_by_city %}
<li><strong>{{ v.city }}&nbsp;:</strong> {{ v.total }}</li>
{% endfor %}
</ul>
</article>
<script>
// prepare data
const data_startday = [
{% for s in stats_by_startday %}
{% if not forloop.first %}, {% endif %}
{date: new Date('{{ s.day.isoformat }}'), value: {{ s.total}}}
{% endfor %}
];
const data_creation = [
{% for s in stats_by_creation %}
{% if not forloop.first %}, {% endif %}
{date: new Date('{{ s.day.isoformat }}'), value: {{ s.total}}}
{% endfor %}
];
// define parameters
const tooltip = {text: function (timestamp, value, dayjsDate) {
if (value != null)
return `${dayjsDate.format('DD/MM/YYYY')}: ${value}`;
else
return `${dayjsDate.format('DD/MM/YYYY')}`;
}};
const calendarlabel = { position: 'left', key: 'left', text: () => ['lun', '', 'mer', '', 'ven', '', 'dim'] };
// set options
var options = {
date: {
locale: 'fr',
start: new Date("{{ first_by_startday.isoformat }}"),
highlight: new Date()
},
domain: {
type: "month",
label: {text: 'MMM YY'},
padding: [3, 3, 3, 3]
},
subDomain: {
type: "day",
width: 20,
height: 20,
radius: 2,
label : function (timestamp, value) { if (value == null) return ''; else return `${value}`; }
},
data: {
source: data_startday,
defaultValue: null,
x: "date",
y: "value"
},
theme: (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? "dark" : "light",
itemSelector: '#cal-heatmap-startday',
animationDuration: 0
};
// create first heatmap
const cal_startday = new CalHeatmap();
cal_startday.paint(options, [
[CalendarLabel, calendarlabel],
[Tooltip, tooltip]
]);
// create second heatmap
options.itemSelector = '#cal-heatmap-creation';
options.date.start = new Date("{{ first_by_creation.isoformat }}");
options.data.source = data_creation;
const cal_creation = new CalHeatmap();
cal_creation.paint(options, [
[CalendarLabel, calendarlabel],
[Tooltip, tooltip]
]);
cal_startday.on('click', (event, timestamp, value) => {
const d = new Date(timestamp);
const url = "{% url 'day_view' 0 0 0 %}".replace("/0/", "/" + d.getFullYear() + "/").replace("/0/", "/" + (d.getMonth() + 1) + "/").replace("/0/", "/" + d.getDate() + "/");
window.location = url;
});
</script>
{% endblock %}

View File

@ -124,6 +124,7 @@ urlpatterns = [
path("merci", thank_you, name="thank_you"),
path("contact", MessageCreateView.as_view(), name="contact"),
path("messages", view_messages, name="messages"),
path("statistiques", statistics, name="statistics"),
path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path(
"message/<int:pk>",

View File

@ -16,7 +16,7 @@ from .utils import PlaceGuesser
import hashlib
from django.core.cache import cache
from django.core.mail import mail_admins
import calendar as _calendar
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
@ -78,7 +78,7 @@ from django.utils import timezone
from django.utils.html import escape
from datetime import date, timedelta
from django.utils.timezone import datetime
from django.db.models import Q, Subquery, OuterRef, Count, F, Func, BooleanField, ExpressionWrapper, When
from django.db.models import Q, Subquery, OuterRef, Count, F, Func, BooleanField, ExpressionWrapper, When, Max
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -2269,6 +2269,45 @@ def view_tag(request, t, past=False):
return render(request, "agenda_culturel/tag.html", context)
def statistics(request):
stats = {}
max = {}
first = {}
last = {}
ev_published = Event.objects.filter(Q(status=Event.STATUS.PUBLISHED) &
(Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)))
for v in ['start_day', 'created_date__date']:
after = 24
last[v] = date.today() if v == 'created_date__date' else date.today() + timedelta(weeks=after)
last[v] = last[v].replace(day = _calendar.monthrange(last[v].year, last[v].month)[1])
r = 8 * 30
if v == 'start_day':
r += after * 7
first[v] = (last[v] - timedelta(days=r)).replace(day=1)
stats[v] = ev_published. \
annotate(day=F(v)). \
filter(Q(day__lte=last[v]) & Q(day__gte=first[v])).values('day').annotate(total=Count('day')).order_by('day')
nb_by_city = ev_published.annotate(city=F('exact_location__city')).filter(city__isnull=False).values('city').annotate(total=Count('city')).order_by('-total')
context = {"stats_by_startday": stats["start_day"], "stats_by_creation": stats["created_date__date"],
"first_by_startday": first["start_day"], "last_by_startday": last["start_day"],
"first_by_creation": first["created_date__date"], "last_by_creation": last["created_date__date"],
"nb_by_city": nb_by_city
}
return render(request, "agenda_culturel/statistics.html", context)
def tag_list(request):
tags = Event.get_all_tags()
r_tags = [t["tag"] for t in tags]