diff --git a/backend/base_app/templates/stats.html b/backend/base_app/templates/stats.html
index 7589427712488cbf9bd99dd14596133dce924807..4504513d3fb0f06028c11900e67a2517bd19007d 100644
--- a/backend/base_app/templates/stats.html
+++ b/backend/base_app/templates/stats.html
@@ -1,11 +1,12 @@
{% extends "base.html" %}
+
{% load render_bundle from webpack_loader %}
{% block content %}
-
+
{% render_bundle 'stats' %}
{% endblock %}
diff --git a/backend/base_app/views.py b/backend/base_app/views.py
index 3293e7df452022c0c5cc0d103aff3ef5ed377ebf..50e6642c4060a53bf385a21586e9838b49f86d70 100644
--- a/backend/base_app/views.py
+++ b/backend/base_app/views.py
@@ -1,8 +1,10 @@
+from datetime import datetime, timedelta
import logging
import json
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
from django.shortcuts import render
+from django.utils.timezone import make_aware
from webpack_loader.utils import get_files
from backend_app.utils import clean_route
@@ -16,6 +18,7 @@ from _cron_tasks import (
clear_and_clean_sessions,
update_daily_stats,
)
+from stats_app.models import DailyConnections, DailyExchangeContributionsInfo
logger = logging.getLogger("django")
@@ -50,7 +53,57 @@ def stats(request):
"""
Render the view that displays stats
"""
- stats_data = []
+ dataset = request.GET.get("dataset")
+
+ now = make_aware(datetime.now())
+ now_minus_365_days = now - timedelta(days=365)
+
+ if dataset == "daily_connections":
+ daily_connections = DailyConnections.objects.filter(
+ date__gte=now_minus_365_days,
+ date__lt=now,
+ )
+ raw_data = [
+ {
+ "date": dc.date.strftime("%Y-%m-%d"),
+ "nb_connections": dc.nb_connections
+ }
+ for dc in daily_connections
+ ]
+
+ cols = ["date", "nb_connections"]
+
+ elif dataset == "daily_exchange_contributions":
+ daily_contributions = DailyExchangeContributionsInfo.objects.filter(
+ date__gte=now_minus_365_days,
+ date__lt=now,
+ ).prefetch_related("university")
+
+ raw_data = [
+ {
+ "date": dc.date.strftime("%Y-%m-%d"),
+ "university": f"{dc.university.pk} - {dc.university.name}",
+ "major": dc.major,
+ "minor": dc.minor,
+ "exchange_semester": dc.exchange_semester,
+ "nb_contributions": dc.nb_contributions,
+ }
+ for dc in daily_contributions
+ ]
+
+ cols = [
+ "date",
+ "university",
+ "major",
+ "minor",
+ "exchange_semester",
+ "nb_contributions",
+ ]
+
+ else:
+ return HttpResponseRedirect("/stats/?dataset=daily_connections")
+
+ stats_data = {c: [d[c] for d in raw_data] for c in cols}
return render(request, "stats.html", dict(stats_data=json.dumps(stats_data)))
diff --git a/frontend/src/components/pages/PageStats.jsx b/frontend/src/components/pages/PageStats.jsx
index 8f4b2a831bb80c51d21c614ebbfe6dce8892c411..00f24246420d0d97a348ded842534760c04e02e5 100644
--- a/frontend/src/components/pages/PageStats.jsx
+++ b/frontend/src/components/pages/PageStats.jsx
@@ -1,51 +1,54 @@
import React from "react";
import { compose } from "recompose";
import Typography from "@material-ui/core/Typography";
-import { Line } from "react-chartjs-2";
-import alasql from "alasql";
+import { makeStyles } from "@material-ui/styles";
+import AppBar from "@material-ui/core/AppBar";
+import Tabs from "@material-ui/core/Tabs";
+import Tab from "@material-ui/core/Tab";
import { withPaddedPaper } from "./shared";
+import SqlInterface from "../stats/RequestSQLHandler";
+import { appBarHeight, siteMaxWidth } from "../../config/sharedStyles";
-const data = [
- { a: 1, b: 1, c: 1 },
- { a: 1, b: 2, c: 1 },
- { a: 1, b: 3, c: 1 },
- { a: 2, b: 1, c: 1 },
-];
-const res = alasql("SELECT a, COUNT(*) AS c FROM ? GROUP BY a", [data]);
-
-// eslint-disable-next-line no-console
-console.log(res);
+const useStyles = makeStyles((theme) => ({
+ requestInterface: {},
+ tabBar: {
+ minHeight: theme.spacing(5),
+ [`@media (min-width:${siteMaxWidth()}px)`]: {
+ top: appBarHeight(theme),
+ },
+ },
+}));
/**
* Component corresponding to the stats page of the site
*/
function PageStats() {
+ const classes = useStyles();
+
+ // TODO faire une fonction handleChange qui change le dataset dans l'url et recharge la page
+
return (
<>
- Vive les stats
-
- More to come...
-
+
+ Exploration des statistiques d'utilisation du site REX-DRI
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/frontend/src/components/stats/Plot.jsx b/frontend/src/components/stats/Plot.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..60617630a407b26d4f972fc4d47bcc36c6e3019c
--- /dev/null
+++ b/frontend/src/components/stats/Plot.jsx
@@ -0,0 +1,102 @@
+import React from "react";
+import { Line } from "react-chartjs-2";
+import PropTypes from "prop-types";
+
+const colors = [
+ "#1f77b4",
+ "#ff7f0e",
+ "#2ca02c",
+ "#d62728",
+ "#9467bd",
+ "#8c564b",
+ "#e377c2",
+ "#7f7f7f",
+ "#bcbd22",
+ "#17becf",
+];
+
+function PlotResult({ result }) {
+ if (
+ !Object.keys(result[0]).includes("x") ||
+ !Object.keys(result[0]).includes("y")
+ ) {
+ return (
+
+
+ Pour afficher les résultats sous forme graphique, vous devez préciser
+ 'x' et 'y' dans le SELECT (pensez à utiliser un ORDER BY sur X). Vous
+ pouvez également préciser des catégories avec 'cat'.
+
+
+ );
+ }
+
+ if (!Object.keys(result[0]).includes("cat")) {
+ const mapping = new Map();
+ result.forEach(({ x, y }) => {
+ mapping.set(x, y);
+ });
+
+ const X = [...mapping.keys()];
+ X.sort();
+
+ return (
+
+ mapping.get(x)),
+ },
+ ],
+ }}
+ />
+
+ );
+ }
+
+ const categories = [...new Set(result.map((el) => el.cat))];
+
+ const mapping = new Map();
+ result.forEach(({ x, y, cat }) => {
+ if (!mapping.has(x)) {
+ mapping.set(x, new Map());
+ }
+
+ mapping.get(x).set(cat, y);
+ });
+
+ const X = [...mapping.keys()];
+ X.sort();
+
+ const datasets = categories.map((cat, index) => ({
+ label: cat,
+ fill: "disabled",
+ borderColor: colors[index % colors.length],
+ data: X.map((x) => {
+ const val = mapping.get(x).get(cat);
+ return typeof val !== "undefined" ? val : 0;
+ }),
+ }));
+
+ return (
+
+
+
+ );
+}
+
+PlotResult.propTypes = {
+ result: PropTypes.array.isRequired,
+};
+
+export default PlotResult;
diff --git a/frontend/src/components/stats/RequestSQLHandler.jsx b/frontend/src/components/stats/RequestSQLHandler.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..bf1ee40dad00e62ef6fd55d0fcb9d5da4c0951f5
--- /dev/null
+++ b/frontend/src/components/stats/RequestSQLHandler.jsx
@@ -0,0 +1,209 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import alasql from "alasql";
+import Typography from "@material-ui/core/Typography";
+import TextField from "@material-ui/core/TextField";
+import Button from "@material-ui/core/Button";
+import { makeStyles } from "@material-ui/styles";
+import TableFromData from "./Table";
+import PlotResult from "./Plot";
+
+// eslint-disable-next-line no-undef
+const DataFromBackend = __StatsData;
+const cols = Object.keys(DataFromBackend);
+const data = [];
+// eslint-disable-next-line no-plusplus
+for (let i = 0; i < DataFromBackend[cols[0]].length; i++) {
+ const elem = {};
+ cols.forEach((col) => {
+ elem[col] = DataFromBackend[col][i];
+ });
+ data.push(elem);
+}
+
+const useStyles = makeStyles({});
+
+// eslint-disable-next-line no-shadow
+function executeRequest(sqlRequest, data) {
+ return alasql.promise(sqlRequest, [data]);
+}
+
+function SqlRequestResult({ result }) {
+ return (
+
+
+
+
+
+
{
+ alasql(
+ 'SELECT * INTO CSV("export_rex_dri.csv", {headers:true}) FROM ?',
+ [result]
+ );
+ }}
+ >
+ Exporter en CSV
+
+
+
+ Pour partager votre recherche il vous suffit de copier l'url de la page !
+
+ );
+}
+
+SqlRequestResult.propTypes = {
+ result: PropTypes.array.isRequired,
+};
+
+const defaultRequest = `SELECT
+ date AS x,
+ nb_connections AS y
+FROM ?
+ORDER BY x ASC;
+`;
+// TODO à changer selon le dataset
+// TODO si plusieurs requetes par défaut proposées pour un meme dataset : les proposer avec des boutons
+/*
+ SELECT
+ date AS x,
+ major AS cat,
+ sum(nb_contributions) AS y
+ FROM ?
+ GROUP BY date, major
+ ORDER BY date ASC;
+*/
+
+/**
+ SELECT
+ date AS x,
+ CASE WHEN
+ major = 'IM' OR major = 'GM' OR major = 'GSM' THEN 'IM'
+ ELSE 'AUTRES'
+ END AS cat,
+ sum(nb_contributions) AS y
+ FROM ?
+ GROUP BY date, major
+ ORDER BY date ASC;
+ */
+
+function getRequestFromUrl() {
+ const getParamsInUrl = new URL(document.location).searchParams;
+ let request = defaultRequest;
+
+ try {
+ const requestInfoString = getParamsInUrl.has("request_info")
+ ? getParamsInUrl.get("request_info")
+ : "";
+ request = JSON.parse(atob(requestInfoString)).request;
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(err);
+ }
+
+ return request;
+}
+
+/**
+ *
+ */
+function setRequestInUrl(request) {
+ const requestInfo = {
+ request,
+ version: 1.0,
+ };
+
+ const requestInfoAsString = btoa(JSON.stringify(requestInfo));
+ const getParamsInUrl = new URL(document.location).searchParams;
+ getParamsInUrl.set("request_info", requestInfoAsString);
+
+ window.history.replaceState(null, null, `?${getParamsInUrl.toString()}`);
+}
+
+// eslint-disable-next-line no-unused-vars
+function getDatasetFromUrl() {
+ const getParamsInUrl = new URL(document.location).searchParams;
+ let datasetName = "daily_connections";
+
+ try {
+ datasetName = getParamsInUrl.has("dataset")
+ ? getParamsInUrl.get("dataset")
+ : "daily_connections";
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log(err);
+ }
+
+ return datasetName;
+}
+
+// eslint-disable-next-line no-unused-vars
+function setDatasetInUrl(datasetName) {
+ const getParamsInUrl = new URL(document.location).searchParams;
+ getParamsInUrl.set("dataset", datasetName);
+
+ window.history.replaceState(null, null, `?${getParamsInUrl.toString()}`);
+}
+
+const requestFromUrl = getRequestFromUrl();
+
+function SqlInterface() {
+ const classes = useStyles();
+
+ const [sqlRequest, setSqlRequest] = useState(requestFromUrl);
+
+ useEffect(() => {
+ setRequestInUrl(sqlRequest);
+ }, [sqlRequest]);
+
+ const [requestError, setRequestError] = useState("");
+ const [requestResult, setRequestResult] = useState([]);
+
+ return (
+
+ setSqlRequest(event.target.value)}
+ />
+ {requestError !== "" && (
+ <>
+
+ {requestError.toString()}
+
+ >
+ )}
+ {
+ setRequestError("");
+ setRequestResult([]);
+ executeRequest(sqlRequest, data)
+ .then((res) => {
+ setRequestResult(res);
+ })
+ .catch((err) => {
+ setRequestError(err);
+ });
+ }}
+ >
+ Exécuter
+
+ {requestResult.length !== 0 && (
+
+ )}
+
+ );
+}
+
+export default SqlInterface;
diff --git a/frontend/src/components/stats/Table.jsx b/frontend/src/components/stats/Table.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed8d69b7c3e2259272facc76978b55de1ed32a03
--- /dev/null
+++ b/frontend/src/components/stats/Table.jsx
@@ -0,0 +1,95 @@
+import React from "react";
+import PropTypes from "prop-types";
+import Paper from "@material-ui/core/Paper";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TablePagination from "@material-ui/core/TablePagination";
+import TableRow from "@material-ui/core/TableRow";
+import { makeStyles } from "@material-ui/styles";
+
+const useStyles = makeStyles({
+ root: {
+ width: "100%",
+ },
+ container: {
+ maxHeight: 440,
+ },
+});
+
+function TableFromData({ data }) {
+ const classes = useStyles();
+ const [page, setPage] = React.useState(0);
+ const [rowsPerPage, setRowsPerPage] = React.useState(10);
+
+ if (data.length === 0) {
+ return <>>;
+ }
+
+ const columns = Object.keys(data[0]).map((id) => ({ id, label: id }));
+
+ const handleChangePage = (event, newPage) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (event) => {
+ setRowsPerPage(+event.target.value);
+ setPage(0);
+ };
+
+ return (
+
+
+
+
+
+ {columns.map((column) => (
+ {column.label}
+ ))}
+
+
+
+ {data
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ .map((row, i) => {
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+ {columns.map((column) => {
+ const value = row[column.id];
+ return (
+
+ {column.format && typeof value === "number"
+ ? column.format(value)
+ : value}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
+
+TableFromData.propTypes = {
+ data: PropTypes.array.isRequired,
+};
+
+export default TableFromData;