diff --git a/assets/js/admin/admin.js b/assets/js/admin/admin.js
index 0bf8794..4430472 100644
--- a/assets/js/admin/admin.js
+++ b/assets/js/admin/admin.js
@@ -23,3 +23,4 @@ require('./modules/sortable.js')()
require('./modules/batch.js')()
require('./modules/file-manager.js')()
require('./modules/file-picker.js')()
+require('./modules/analytics.js')()
diff --git a/assets/js/admin/modules/analytics.js b/assets/js/admin/modules/analytics.js
new file mode 100644
index 0000000..dcb6d1a
--- /dev/null
+++ b/assets/js/admin/modules/analytics.js
@@ -0,0 +1,53 @@
+const $ = require('jquery')
+const Chart = require('chart.js/auto').default
+
+const drawChart = () => {
+ const ctx = document.getElementById('analytic-chart')
+ const options = {
+ type: 'bar',
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ beginAtZero: true
+ }
+ }
+ },
+ data: {
+ labels: JSON.parse(ctx.getAttribute('data-labels')),
+ datasets: [{
+ label: ctx.getAttribute('data-label'),
+ data: JSON.parse(ctx.getAttribute('data-values')),
+ backgroundColor: 'rgba(54, 162, 235, 0.2)',
+ borderColor: 'rgb(54, 162, 235)',
+ borderWidth: 1
+ }]
+ }
+ }
+
+ const chart = new Chart(ctx, options)
+
+ const resize = () => {
+ const width = ctx.parentNode.parentNode.offsetWidth
+ const height = 250
+
+ chart.resize(width, height)
+ }
+
+ resize()
+
+ window.addEventListener('resize', resize)
+}
+
+module.exports = () => {
+ const body = $('body')
+
+ body.on('shown.bs.modal', '.modal', (e) => {
+ window.setTimeout(() => {
+ if (document.getElementById('analytic-chart')) {
+ drawChart()
+ }
+ }, 500)
+ })
+}
diff --git a/assets/js/admin/modules/modal.js b/assets/js/admin/modules/modal.js
index 8afb28a..13cecba 100644
--- a/assets/js/admin/modules/modal.js
+++ b/assets/js/admin/modules/modal.js
@@ -3,8 +3,10 @@ const $ = require('jquery')
const openModal = function (url) {
let container = $('#modal-container')
const body = $('body')
+ let doTrigger = true
if (!container.length) {
+ let doTrigger = false
container = $('
')
body.append(container)
@@ -20,6 +22,10 @@ const openModal = function (url) {
container.load(url, function () {
loader.remove()
+
+ if (doTrigger) {
+ container.trigger('shown.bs.modal')
+ }
})
}
diff --git a/core/Analytic/RangeAnalytic.php b/core/Analytic/RangeAnalytic.php
new file mode 100644
index 0000000..a4d5ef6
--- /dev/null
+++ b/core/Analytic/RangeAnalytic.php
@@ -0,0 +1,133 @@
+
+ */
+class RangeAnalytic
+{
+ protected ViewRepositoryQuery $viewQuery;
+ protected RefererRepositoryQuery $refererQuery;
+
+ public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery)
+ {
+ $this->viewQuery = $viewQuery;
+ $this->refererQuery = $refererQuery;
+ }
+
+ public function getViews(\DateTime $from, \DateTime $to, Node $node): array
+ {
+ $entities = $this->viewQuery->create()
+ ->andWhere('.date >= :from')
+ ->andWhere('.date <= :to')
+ ->andWhere('.node = :node')
+ ->orderBy('.date')
+ ->setParameters([
+ ':from' => $from,
+ ':to' => $to,
+ ':node' => $node->getId(),
+ ])
+ ->find()
+ ;
+
+ $diff = $from->diff($to);
+
+ if ($diff->days >= 365) {
+ $format = 'Y-m';
+ } else {
+ $format = 'Y-m-d';
+ }
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = $entity->getDate()->format($format);
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = 0;
+ }
+
+ $datas[$index] += $entity->getViews();
+ }
+
+ return $datas;
+ }
+
+ public function getPathViews(\DateTime $from, \DateTime $to, Node $node): array
+ {
+ $entities = $this->viewQuery->create()
+ ->andWhere('.date >= :from')
+ ->andWhere('.date <= :to')
+ ->andWhere('.node = :node')
+ ->orderBy('.date')
+ ->setParameters([
+ ':from' => $from,
+ ':to' => $to,
+ ':node' => $node->getId(),
+ ])
+ ->find()
+ ;
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = $entity->getPath();
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = 0;
+ }
+
+ $datas[$index] += $entity->getViews();
+ }
+
+ return $datas;
+ }
+
+ public function getReferers(\DateTime $from, \DateTime $to, Node $node): array
+ {
+ $entities = $this->refererQuery->create()
+ ->andWhere('.date >= :from')
+ ->andWhere('.date <= :to')
+ ->andWhere('.node = :node')
+ ->orderBy('.date')
+ ->setParameters([
+ ':from' => $from,
+ ':to' => $to,
+ ':node' => $node->getId(),
+ ])
+ ->find()
+ ;
+
+ $datas = [];
+
+ foreach ($entities as $entity) {
+ $index = parse_url($entity->getUri(), PHP_URL_HOST);
+
+ if (!isset($datas[$index])) {
+ $datas[$index] = [
+ 'views' => 0,
+ 'uris' => [],
+ ];
+ }
+
+ $datas[$index]['views'] += $entity->getViews();
+
+ $path = parse_url($entity->getUri(), PHP_URL_PATH);
+
+ if (!isset($datas[$index]['uris'][$path])) {
+ $datas[$index]['uris'][$path] = 0;
+ }
+
+ $datas[$index]['uris'][$path] += $entity->getViews();
+ }
+
+ return $datas;
+ }
+}
diff --git a/core/Controller/Analytic/AnalyticController.php b/core/Controller/Analytic/AnalyticController.php
new file mode 100644
index 0000000..83b9640
--- /dev/null
+++ b/core/Controller/Analytic/AnalyticController.php
@@ -0,0 +1,37 @@
+createNotFoundException();
+ }
+
+ $views = $rangeAnalytic->getViews(new \DateTime('now - '.$range), new \DateTime(), $node);
+ $pathViews = $rangeAnalytic->getPathViews(new \DateTime('now - '.$range), new \DateTime(), $node);
+ $referers = $rangeAnalytic->getReferers(new \DateTime('now - '.$range), new \DateTime(), $node);
+
+ return $this->render('@Core/analytic/stats.html.twig', [
+ 'range' => $range,
+ 'views' => $views,
+ 'pathViews' => $pathViews,
+ 'referers' => $referers,
+ 'node' => $node,
+ ]);
+ }
+}
diff --git a/core/Controller/Site/NodeAdminController.php b/core/Controller/Site/NodeAdminController.php
index 267cf68..327bd34 100644
--- a/core/Controller/Site/NodeAdminController.php
+++ b/core/Controller/Site/NodeAdminController.php
@@ -21,11 +21,12 @@ use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
/**
* @Route("/admin/site/node")
*/
-class NodeAdminController extends AdminController
+class NodeAdminController extends AbstractController
{
/**
* @Route("/new/{node}", name="admin_site_node_new")
@@ -268,11 +269,6 @@ class NodeAdminController extends AdminController
]);
}
- public function getSection(): string
- {
- return '';
- }
-
protected function handlePageAssociation(
string $pageAction,
?Page $pageEntity,
diff --git a/core/Entity/Analytic/Referer.php b/core/Entity/Analytic/Referer.php
index 0281d3c..1c8b94d 100644
--- a/core/Entity/Analytic/Referer.php
+++ b/core/Entity/Analytic/Referer.php
@@ -18,28 +18,28 @@ class Referer implements EntityInterface
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
- private $id;
+ protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
- private $node;
+ protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
- private $uri;
+ protected $uri;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
- private $views = 0;
+ protected $views = 0;
/**
* @ORM\Column(type="date")
*/
- private $date;
+ protected $date;
public function getId(): ?int
{
diff --git a/core/Entity/Analytic/View.php b/core/Entity/Analytic/View.php
index a5db1b9..0bd3338 100644
--- a/core/Entity/Analytic/View.php
+++ b/core/Entity/Analytic/View.php
@@ -18,28 +18,28 @@ class View implements EntityInterface
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
- private $id;
+ protected $id;
/**
* @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews")
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
*/
- private $node;
+ protected $node;
/**
* @ORM\Column(type="string", length=255)
*/
- private $path;
+ protected $path;
/**
* @ORM\Column(type="integer", options={"default"=0})
*/
- private $views = 0;
+ protected $views = 0;
/**
* @ORM\Column(type="date")
*/
- private $date;
+ protected $date;
public function getId(): ?int
{
diff --git a/core/Entity/Site/Node.php b/core/Entity/Site/Node.php
index 35a684e..4196966 100644
--- a/core/Entity/Site/Node.php
+++ b/core/Entity/Site/Node.php
@@ -145,7 +145,7 @@ class Node implements EntityInterface
/**
* @ORM\Column(type="boolean", options={"default"=0})
*/
- protected $enableAnalytic = false;
+ protected $enableAnalytics = false;
/**
* @ORM\OneToMany(targetEntity=View::class, mappedBy="node")
@@ -569,14 +569,14 @@ class Node implements EntityInterface
return $this;
}
- public function getEnableAnalytic(): ?bool
+ public function getEnableAnalytics(): ?bool
{
- return $this->enableAnalytic;
+ return $this->enableAnalytics;
}
- public function setEnableAnalytic(bool $enableAnalytic): self
+ public function setEnableAnalytics(bool $enableAnalytics): self
{
- $this->enableAnalytic = $enableAnalytic;
+ $this->enableAnalytics = $enableAnalytics;
return $this;
}
diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php
index eb6b983..6154378 100644
--- a/core/EventListener/AnalyticListener.php
+++ b/core/EventListener/AnalyticListener.php
@@ -63,7 +63,7 @@ class AnalyticListener
$node = $this->nodeRepository->findOneBy([
'id' => $request->attributes->get('_node'),
- 'enableViewCounter' => true,
+ 'enableAnalytics' => true,
]);
if (!$node) {
diff --git a/core/Form/Site/NodeType.php b/core/Form/Site/NodeType.php
index 4f41c1d..284d0f9 100644
--- a/core/Form/Site/NodeType.php
+++ b/core/Form/Site/NodeType.php
@@ -61,10 +61,10 @@ class NodeType extends AbstractType
);
$builder->add(
- 'enableAnalytic',
+ 'enableAnalytics',
CheckboxType::class,
[
- 'label' => 'Enable analytic',
+ 'label' => 'Enable analytics',
'required' => false,
'attr' => [
],
diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml
index 7f02595..039b2cc 100644
--- a/core/Resources/translations/messages.fr.yaml
+++ b/core/Resources/translations/messages.fr.yaml
@@ -39,7 +39,8 @@
"Never": "Jamais"
"URL": "URL"
"Disable URL": "Désactiver l'URL"
-"Enable analytic": "Activer l'analyse"
+"Enable analytics": "Activer les analyses"
+"Analytics": "Analyses"
"Controller": "Contrôleur"
"Leave blank for automatic generation": "Laisser vide pour une génération automatique"
"Leave blank to use the default one. Example: App\\Controller\\FooController::barAction": "Laisser vide pour utiliser celui par défaut. Exemple : App\\Controller\\FooController::barAction"
diff --git a/core/Resources/views/analytic/stats.html.twig b/core/Resources/views/analytic/stats.html.twig
new file mode 100644
index 0000000..b1bf18d
--- /dev/null
+++ b/core/Resources/views/analytic/stats.html.twig
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+ {% set values = [] %}
+ {% for item in views %}{% set values = values|merge([item]) %}{% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'Path'|trans }} |
+ {{ 'Views'|trans }} |
+
+
+
+ {% for path, views in pathViews %}
+
+ {{ path }} |
+ {{ views }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ {{ 'Referers'|trans }} |
+ {{ 'Views'|trans }} |
+
+
+
+ {% for referer, info in referers %}
+
+
+
+
+
+ {{ info.views }}
+
+
+ {{ referer }}
+
+
+ {% for path, views in info.uris %}
+
+
+ {{ info.views }}
+
+
+ {{ path }}
+
+ {% endfor %}
+
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
diff --git a/core/Resources/views/site/tree_admin/navigation.html.twig b/core/Resources/views/site/tree_admin/navigation.html.twig
index b077da6..f49a211 100644
--- a/core/Resources/views/site/tree_admin/navigation.html.twig
+++ b/core/Resources/views/site/tree_admin/navigation.html.twig
@@ -72,6 +72,7 @@
{% set move = path('admin_site_node_move', {entity: node.id}) %}
{% set edit = path('admin_site_node_edit', {entity: node.id}) %}
{% set new = path('admin_site_node_new', {node: node.id}) %}
+ {% set analytics = path('admin_analytic_stats', {node: node.id}) %}
@@ -147,6 +148,10 @@
{% endif %}
+
+
diff --git a/package.json b/package.json
index 94ed59a..5abf753 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"@fortawesome/fontawesome-free": "^5.11.2",
"axios": "^0.21.1",
"bootstrap": "^4.3.1",
+ "chart.js": "^3.7.1",
"choices.js": "^9.0.1",
"flag-icon-css": "^3.5.0",
"flatpickr": "^4.6.9",
diff --git a/yarn.lock b/yarn.lock
index c9d359d..f23a676 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1883,6 +1883,11 @@ chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+chart.js@^3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada"
+ integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA==
+
choices.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"