From 17c81a98a63289d00c67ea02abbd0ca426493e69 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Jan 2022 22:03:23 +0100 Subject: [PATCH 001/237] update Makefile envvar --- Makefile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 393e17c..3ad92f2 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,30 @@ -COMPOSER ?= composer -PHP ?= php8.1 -SSH ?= ssh -WEBPACK ?= webpack -YARN ?= yarn +COMPOSER_BIN ?= composer +PHP_BIN ?= php8.1 +SSH_BIN ?= ssh +WEBPACK_BIN ?= webpack +YARN_BIN ?= yarn all: dep asset clean .ONESHELL: dep: - $(COMPOSER) update --ignore-platform-reqs - $(COMPOSER) install --ignore-platform-reqs - $(YARN) + $(COMPOSER_BIN) update --ignore-platform-reqs + $(COMPOSER_BIN) install --ignore-platform-reqs + $(YARN_BIN) asset-watch: - $(WEBPACK) -w + $(WEBPACK_BIN) -w asset: js-routing - $(YARN) - $(WEBPACK) + $(YARN_BIN) + $(WEBPACK_BIN) js-routing: - $(PHP) bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json + $(PHP_BIN) bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json clean: rm -fr var/cache/dev/* rm -fr var/cache/prod/* doctrine-migration: - PHP=$(PHP) ./bin/doctrine-migrate + PHP=$(PHP_BIN) ./bin/doctrine-migrate From 683b9154c9e53874c52a6bcdb93ebdc9499e6647 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Jan 2022 22:04:49 +0100 Subject: [PATCH 002/237] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6db227..34bc811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ ## [Unreleased] +### Fixed +* fix Makefile environment vars (renaming) + ## 1.0.0 From 0ac49a693092b3dafac064a059bc4c8f64d6cc73 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Jan 2022 22:07:40 +0100 Subject: [PATCH 003/237] add interface in the user entity --- src/Entity/User.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index ac74eb7..447a323 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -7,13 +7,14 @@ use App\Core\Entity\EntityInterface; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass=UserRepository::class) * @ORM\HasLifecycleCallbacks() */ -class User implements UserInterface, TwoFactorInterface, EntityInterface +class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFactorInterface, EntityInterface { use Timestampable; /** From 0984edf4622fd1eb7e680cc328e88a119704cb82 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Jan 2022 22:15:53 +0100 Subject: [PATCH 004/237] fix missing implements in the user entity --- src/Entity/User.php | 25 ++++++++----------------- src/Repository/UserRepository.php | 3 ++- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index 447a323..ba428bc 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -84,6 +84,14 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return $this->id; } + /** + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return $this->getUsername(); + } + public function getEmail(): ?string { return $this->email; @@ -96,11 +104,6 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return $this; } - /** - * A visual identifier that represents this user. - * - * @see UserInterface - */ public function getUsername(): string { return (string) $this->email; @@ -131,9 +134,6 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return $this; } - /** - * @see UserInterface - */ public function getPassword(): string { return (string) $this->password; @@ -146,20 +146,11 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return $this; } - /** - * Returning a salt is only needed, if you are not using a modern - * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml. - * - * @see UserInterface - */ public function getSalt(): ?string { return null; } - /** - * @see UserInterface - */ public function eraseCredentials() { // If you store any temporary, sensitive data on the user, clear it here diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index b7bead5..570efd5 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -8,6 +8,7 @@ use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { @@ -16,7 +17,7 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader parent::__construct($registry, User::class); } - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newEncodedPassword): void { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); From a038ee50f471306e7bbe471cbfb5ee1bf2bf3830 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 24 Jan 2022 22:33:07 +0100 Subject: [PATCH 005/237] fix deprecated --- config/packages/security.yaml | 8 ++++---- core/Bundle/CoreBundle.php | 3 ++- src/Bundle/AppBundle.php | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9665acd..225caea 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -34,10 +34,10 @@ security: logout: path: auth_logout target: / - remember_me: - secret: '%kernel.secret%' - lifetime: 604800 - path: / + # remember_me: + # secret: '%kernel.secret%' + # lifetime: 604800 + # path: / # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used diff --git a/core/Bundle/CoreBundle.php b/core/Bundle/CoreBundle.php index ad40278..034a544 100644 --- a/core/Bundle/CoreBundle.php +++ b/core/Bundle/CoreBundle.php @@ -13,10 +13,11 @@ namespace App\Core\Bundle; use App\Core\DependencyInjection\CoreExtension; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; class CoreBundle extends Bundle { - public function getContainerExtension() + public function getContainerExtension(): ?ExtensionInterface { return new CoreExtension(); } diff --git a/src/Bundle/AppBundle.php b/src/Bundle/AppBundle.php index 06b408c..a068ff3 100644 --- a/src/Bundle/AppBundle.php +++ b/src/Bundle/AppBundle.php @@ -13,10 +13,11 @@ namespace App\Bundle; use App\DependencyInjection\AppExtension; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; class AppBundle extends Bundle { - public function getContainerExtension() + public function getContainerExtension(): ?ExtensionInterface { return new AppExtension(); } From 60879667b8c8602ec9228bc3cde34c28e01c2479 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 25 Jan 2022 09:08:05 +0100 Subject: [PATCH 006/237] fix packaging issue (composer minimum stability) --- composer.json | 2 +- config/packages/security.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 155641c..de5e0af 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "A powerful CMS framework", "type": "project", "license": "MIT", - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true, "require": { "php": ">=8.0.0", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 225caea..9665acd 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -34,10 +34,10 @@ security: logout: path: auth_logout target: / - # remember_me: - # secret: '%kernel.secret%' - # lifetime: 604800 - # path: / + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used From 0f91cda7b333031442b5f87fcf090916d4f01dd7 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 25 Jan 2022 09:15:00 +0100 Subject: [PATCH 007/237] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bc811..51d273d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## [Unreleased] +## 1.0.1 ### Fixed * fix Makefile environment vars (renaming) +* fix composer minimum stability ## 1.0.0 From ee0fc45e5cd6060bccc2129dc6b68610b216aafb Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 25 Jan 2022 22:46:21 +0100 Subject: [PATCH 008/237] remove dev code --- core/Form/FileManager/FileUploadType.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/Form/FileManager/FileUploadType.php b/core/Form/FileManager/FileUploadType.php index 989660b..fec3fd0 100644 --- a/core/Form/FileManager/FileUploadType.php +++ b/core/Form/FileManager/FileUploadType.php @@ -21,9 +21,6 @@ class FileUploadType extends AbstractType 'required' => true, 'multiple' => true, 'attr' => [ - 'webkitdirectory' => '', - 'directory' => '', - 'mozdirectory' => '', ], 'constraints' => [ new All([ From 87f2a51e278d0f9f1cc5d55b25649d5246d404e0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 26 Jan 2022 23:10:43 +0100 Subject: [PATCH 009/237] add directory upload in file manager --- .../FileManagerAdminController.php | 15 +++++++++++- core/FileManager/FsFileManager.php | 12 +++++++--- core/Form/FileManager/FileUploadType.php | 24 ++++++++++++++++++- core/Resources/translations/messages.fr.yaml | 1 + 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/core/Controller/FileManager/FileManagerAdminController.php b/core/Controller/FileManager/FileManagerAdminController.php index ec7677b..e9fb422 100644 --- a/core/Controller/FileManager/FileManagerAdminController.php +++ b/core/Controller/FileManager/FileManagerAdminController.php @@ -275,7 +275,20 @@ class FileManagerAdminController extends AdminController $form->handleRequest($request); if ($form->isValid()) { - $manager->upload($form->get('files')->getData(), $request->query->get('file')); + if ($form->get('files')->getData()) { + $manager->upload( + $form->get('files')->getData(), + $request->query->get('file') + ); + } + + if ($form->get('directory')->getData()) { + $manager->upload( + $form->get('directory')->getData(), + $request->query->get('file'), + $_FILES['file_upload']['full_path']['directory'] ?? [] + ); + } if (!$request->isXmlHttpRequest()) { $this->addFlash('success', 'Files uploaded.'); diff --git a/core/FileManager/FsFileManager.php b/core/FileManager/FsFileManager.php index f2f77ea..57d1d74 100644 --- a/core/FileManager/FsFileManager.php +++ b/core/FileManager/FsFileManager.php @@ -195,14 +195,20 @@ class FsFileManager return true; } - public function upload($files, string $path) + public function upload($files, string $path, array $fullPaths = []) { if ($files instanceof UploadedFile) { $files = [$files]; } - foreach ($files as $file) { - $this->uploadHandler->handleForm($file, $this->path.'/'.$path, null, true); + foreach ($files as $key => $file) { + $directory = $this->path.'/'.$path; + + if (isset($fullPaths[$key])) { + $directory .= '/'.trim(dirname($fullPaths[$key]), '/'); + } + + $this->uploadHandler->handleForm($file, $directory, null, true); } } diff --git a/core/Form/FileManager/FileUploadType.php b/core/Form/FileManager/FileUploadType.php index fec3fd0..b24bbcc 100644 --- a/core/Form/FileManager/FileUploadType.php +++ b/core/Form/FileManager/FileUploadType.php @@ -18,7 +18,7 @@ class FileUploadType extends AbstractType FileType::class, [ 'label' => 'Files', - 'required' => true, + 'required' => false, 'multiple' => true, 'attr' => [ ], @@ -31,6 +31,28 @@ class FileUploadType extends AbstractType ], ] ); + + $builder->add( + 'directory', + FileType::class, + [ + 'label' => 'Directory', + 'required' => false, + 'multiple' => true, + 'attr' => [ + 'webkitdirectory' => '', + 'mozdirectory' => '', + 'directory' => '', + ], + 'constraints' => [ + new All([ + new File([ + 'mimeTypes' => $options['mimes'], + ]), + ]), + ], + ] + ); } public function configureOptions(OptionsResolver $resolver) diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml index 780f9e3..6a23fc8 100644 --- a/core/Resources/translations/messages.fr.yaml +++ b/core/Resources/translations/messages.fr.yaml @@ -192,3 +192,4 @@ "Attributes": "Attributs" "Choose": "Choisir" "Associated": "Associé(e)" +"Directory": "Répertoire" From 44b60922b43535ad4549402403d1967e9c5e4e0f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 26 Jan 2022 23:13:57 +0100 Subject: [PATCH 010/237] fix parameter order --- core/Controller/Site/NodeAdminController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/Controller/Site/NodeAdminController.php b/core/Controller/Site/NodeAdminController.php index f84581d..267cf68 100644 --- a/core/Controller/Site/NodeAdminController.php +++ b/core/Controller/Site/NodeAdminController.php @@ -104,12 +104,12 @@ class NodeAdminController extends AdminController */ public function edit( Entity $entity, - string $tab = 'content', EntityManager $entityManager, PageFactory $pageFactory, PageLocator $pageLocator, ControllerLocator $controllerLocator, - Request $request + Request $request, + string $tab = 'content' ): Response { $form = $this->createForm(EntityType::class, $entity, [ 'pages' => $pageLocator->getPages(), From d56f9bd6d4d00f70cf1a93d5f20a273795f01dda Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 26 Jan 2022 23:20:21 +0100 Subject: [PATCH 011/237] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d273d..570ec0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## [Unreleased] +### Added +* add directory upload in file manager + +### Fixed +* fix admin node routing + ## 1.0.1 ### Fixed * fix Makefile environment vars (renaming) From ff6753854378d5701d460b7324de455753fe4ca3 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 28 Jan 2022 14:43:03 +0100 Subject: [PATCH 012/237] remove the port from domain (SiteRequest) --- core/Site/SiteRequest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/Site/SiteRequest.php b/core/Site/SiteRequest.php index 07cf2ee..e127eeb 100644 --- a/core/Site/SiteRequest.php +++ b/core/Site/SiteRequest.php @@ -69,7 +69,9 @@ class SiteRequest public function getDomain(): string { - return $this->requestStack->getCurrentRequest()->headers->get('host'); + $host = $this->requestStack->getCurrentRequest()->headers->get('host'); + + return preg_replace('/:\d+$/', '', $host); } public function getUri(): string From 5c5e114eb7ce341f69b7e09715b0eb17e46c427c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 29 Jan 2022 21:45:13 +0100 Subject: [PATCH 013/237] remove useless comments --- src/Entity/User.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Entity/User.php b/src/Entity/User.php index ba428bc..1f6c86a 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -84,9 +84,6 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return $this->id; } - /** - * @see UserInterface - */ public function getUserIdentifier(): string { return $this->getUsername(); @@ -109,9 +106,6 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact return (string) $this->email; } - /** - * @see UserInterface - */ public function getRoles(): array { $roles = []; From 990f18c42340a5cab30fe0f17fd70a510dadfb39 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 29 Jan 2022 22:15:53 +0100 Subject: [PATCH 014/237] remove swiftmailer and add symfony/mailer --- .env | 13 +-- composer.json | 1 - config/bundles.php | 1 - config/packages/dev/swiftmailer.yaml | 4 - config/packages/mailer.yaml | 2 + config/packages/swiftmailer.yaml | 4 - config/packages/test/swiftmailer.yaml | 2 - core/Notification/MailNotifier.php | 154 +++++--------------------- symfony.lock | 17 --- 9 files changed, 33 insertions(+), 165 deletions(-) delete mode 100644 config/packages/dev/swiftmailer.yaml delete mode 100644 config/packages/swiftmailer.yaml delete mode 100644 config/packages/test/swiftmailer.yaml diff --git a/.env b/.env index d1b3b36..dcac2e3 100644 --- a/.env +++ b/.env @@ -15,9 +15,13 @@ ###> symfony/framework-bundle ### APP_ENV=dev -APP_SECRET=e6e287f176fe2c69112fc620e1801bf0 +APP_SECRET= ###< symfony/framework-bundle ### +###> symfony/mailer ### +MAILER_DSN=smtp://localhost +###< symfony/mailer ### + ###> doctrine/doctrine-bundle ### # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml @@ -26,10 +30,3 @@ DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" ###< doctrine/doctrine-bundle ### - -###> symfony/swiftmailer-bundle ### -# For Gmail as a transport, use: "gmail://username:password@localhost" -# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" -# Delivery is disabled by default via "null://localhost" -MAILER_URL=null://localhost -###< symfony/swiftmailer-bundle ### diff --git a/composer.json b/composer.json index de5e0af..8621344 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,6 @@ "symfony/security-bundle": "5.4.*", "symfony/serializer": "5.4.*", "symfony/string": "5.4.*", - "symfony/swiftmailer-bundle": "^3.5", "symfony/translation": "5.4.*", "symfony/twig-bundle": "^5.2", "symfony/validator": "5.4.*", diff --git a/config/bundles.php b/config/bundles.php index ace76a4..15f9428 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,7 +13,6 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], diff --git a/config/packages/dev/swiftmailer.yaml b/config/packages/dev/swiftmailer.yaml deleted file mode 100644 index b98158e..0000000 --- a/config/packages/dev/swiftmailer.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# See https://symfony.com/doc/current/email/dev_environment.html -swiftmailer: - # send all emails to a specific address - #delivery_addresses: ['me@example.com'] diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml index 56a650d..d6863f8 100644 --- a/config/packages/mailer.yaml +++ b/config/packages/mailer.yaml @@ -1,3 +1,5 @@ framework: mailer: dsn: '%env(MAILER_DSN)%' + headers: + From: 'Example ' diff --git a/config/packages/swiftmailer.yaml b/config/packages/swiftmailer.yaml deleted file mode 100644 index c06f0bd..0000000 --- a/config/packages/swiftmailer.yaml +++ /dev/null @@ -1,4 +0,0 @@ -swiftmailer: - url: '%env(MAILER_URL)%' - spool: { type: 'memory' } - sender_address: system@localhost diff --git a/config/packages/test/swiftmailer.yaml b/config/packages/test/swiftmailer.yaml deleted file mode 100644 index f438078..0000000 --- a/config/packages/test/swiftmailer.yaml +++ /dev/null @@ -1,2 +0,0 @@ -swiftmailer: - disable_delivery: true diff --git a/core/Notification/MailNotifier.php b/core/Notification/MailNotifier.php index 63aefd4..c4fc96a 100644 --- a/core/Notification/MailNotifier.php +++ b/core/Notification/MailNotifier.php @@ -2,9 +2,8 @@ namespace App\Core\Notification; -use Swift_Attachment; -use Swift_Mailer; -use Swift_Message; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mailer\MailerInterface; use Twig\Environment as TwigEnvironment; /** @@ -14,56 +13,20 @@ use Twig\Environment as TwigEnvironment; */ class MailNotifier { - /** - * @var Swift_Mailer - */ - protected $mailer; + protected MailerInterface $mailer; + protected array $attachments = []; + protected array $recipients = []; + protected array $bccRecipients = []; + protected ?string $subject = null; + protected ?string $from = null; + protected ?string $replyTo = null; - /** - * @var array - */ - protected $attachments = []; - - /** - * @var array - */ - protected $recipients = []; - - /** - * @var array - */ - protected $bccRecipients = []; - - /** - * @var string - */ - protected $subject; - - /** - * @var string - */ - protected $from; - - /** - * @var string - */ - protected $replyTo; - - /** - * Constructor. - * - * @param BasicNotifier $basicNotifier - * @param Swift_Mailer $mail - */ - public function __construct(TwigEnvironment $twig, Swift_Mailer $mailer) + public function __construct(TwigEnvironment $twig, MailerInterface $mailer) { $this->mailer = $mailer; $this->twig = $twig; } - /** - * @return EmailNotifier - */ public function setMailer(Swift_Mailer $mailer): self { $this->mailer = $mailer; @@ -76,9 +39,6 @@ class MailNotifier return $this->mailer; } - /** - * @return EmailNotifier - */ public function setRecipients(array $recipients): self { $this->recipients = $recipients; @@ -91,9 +51,6 @@ class MailNotifier return $this->recipients; } - /** - * @return EmailNotifier - */ public function setBccRecipients(array $bccRecipients): self { $this->bccRecipients = $bccRecipients; @@ -106,11 +63,6 @@ class MailNotifier return $this->bccRecipients; } - /** - * @param string $subject - * - * @return EmailNotifier - */ public function setSubject(?string $subject): self { $this->subject = $subject; @@ -123,11 +75,6 @@ class MailNotifier return $this->subject; } - /** - * @param mixed $from - * - * @return EmailNotifier - */ public function setFrom($from): self { $this->from = $from; @@ -135,21 +82,11 @@ class MailNotifier return $this; } - /** - * @return mixed - */ public function getFrom(): ?string { return $this->from; } - /** - * Set the value of "replyTo". - * - * @param string $replyTo - * - * @return EmailNotifier - */ public function setReplyTo($replyTo): self { $this->replyTo = $replyTo; @@ -157,19 +94,11 @@ class MailNotifier return $this; } - /* - * Get the value of "replyTo". - * - * @return string - */ public function getReplyTo(): ?string { return $this->replyTo; } - /** - * @return EmailNotifier - */ public function setAttachments(array $attachments): self { $this->attachments = $attachments; @@ -182,9 +111,6 @@ class MailNotifier return $this->attachments; } - /** - * @return EmailNotifier - */ public function addRecipient(string $email, bool $isBcc = false): self { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { @@ -204,9 +130,6 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function addRecipients(array $emails, bool $isBcc = false): self { foreach ($emails as $email) { @@ -216,19 +139,11 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function addRecipientByAccount(Account $account, bool $isBcc = false): self { return $this->addRecipient($account->getEmail(), $isBcc); } - /** - * @param mixed $accounts - * - * @return EmailNotifier - */ public function addRecipientsByAccounts($accounts, bool $isBcc = false) { if (!is_array($accounts)) { @@ -242,9 +157,6 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function addAttachment(string $attachment): self { if (!in_array($attachment, $this->attachments)) { @@ -254,9 +166,6 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function addAttachments(array $attachments): self { foreach ($attachments as $attachment) { @@ -266,9 +175,6 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function init(): self { $this @@ -281,58 +187,50 @@ class MailNotifier return $this; } - /** - * @return EmailNotifier - */ public function notify(string $template, array $data = [], string $type = 'text/html'): self { - $message = $this->createMessage( - $this->twig->render( - $template, - $data - ), - $type - ); + $message = $this->createMessage(); + $message->context($data); + + if (in_array($type, ['text/plain', 'text'])) { + $message->textTemplate($template); + } else { + $message->htmlTemplate($template); + } $this->mailer->send($message); return $this; } - protected function createMessage(string $body, string $type = 'text/html'): Swift_Message + protected function createMessage(): TemplatedEmail { - $message = new Swift_Message(); + $message = new TemplatedEmail(); if ($this->getSubject()) { - $message->setSubject($this->getSubject()); + $message->subject($this->getSubject()); } if ($this->getFrom()) { - $message->setFrom($this->getFrom()); + $message->from($this->getFrom()); } if ($this->getReplyTo()) { - $message->setReplyTo($this->getReplyTo()); + $message->replyTo($this->getReplyTo()); } if (count($this->getRecipients()) > 0) { - $message->setTo($this->getRecipients()); + $message->to(...$this->getRecipients()); } if (count($this->getBccRecipients()) > 0) { - $message->setBcc($this->getBccRecipients()); + $message->bcc(...$this->getBccRecipients()); } foreach ($this->getAttachments() as $attachment) { - if (is_object($attachment) && $attachment instanceof Swift_Attachment) { - $message->attach($attachment); - } elseif (is_string($attachment) && file_exists($attachment) && is_readable($attachment) && !is_dir($attachment)) { - $message->attach(Swift_Attachment::fromPath($attachment)); - } + $message->attachFromPath($attachment); } - $message->setBody($body, $type); - return $message; } } diff --git a/symfony.lock b/symfony.lock index 9393b2f..7e97f5e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -269,9 +269,6 @@ "config/packages/stof_doctrine_extensions.yaml" ] }, - "swiftmailer/swiftmailer": { - "version": "v6.2.7" - }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -582,20 +579,6 @@ "symfony/string": { "version": "v5.2.4" }, - "symfony/swiftmailer-bundle": { - "version": "2.5", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "2.5", - "ref": "ae4d22af30bbd484506bc1817c5a3ef72c855b93" - }, - "files": [ - "config/packages/dev/swiftmailer.yaml", - "config/packages/swiftmailer.yaml", - "config/packages/test/swiftmailer.yaml" - ] - }, "symfony/test-pack": { "version": "v1.0.7" }, From 475b907ff5f9eaa8e5f1e152965e1182e649d54b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 29 Jan 2022 23:15:29 +0100 Subject: [PATCH 015/237] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 570ec0d..79df2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ ## [Unreleased] +## 1.1.0 ### Added * add directory upload in file manager ### Fixed * fix admin node routing +### Changed +* symfony/swiftmailer-bundle is replaced by symfony/mailer + ## 1.0.1 ### Fixed * fix Makefile environment vars (renaming) From 610c27072a02cc693a3ea89ecce8c79cced3bb5c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 13 Feb 2022 16:19:08 +0100 Subject: [PATCH 016/237] add sort in file manager --- .../admin/components/file-manager/Files.vue | 34 +++++++++++++++++++ .../FileManagerAdminController.php | 7 +++- core/FileManager/FsFileManager.php | 22 +++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/assets/js/admin/components/file-manager/Files.vue b/assets/js/admin/components/file-manager/Files.vue index edc575e..efb660a 100644 --- a/assets/js/admin/components/file-manager/Files.vue +++ b/assets/js/admin/components/file-manager/Files.vue @@ -23,6 +23,14 @@ {{ form_row(form.disableUrl) }} + {{ form_row(form.enableViewCounter) }} {{ form_row(form.code) }} {{ form_row(form.contentType) }} {{ form_row(form.controller) }} From 1cb57a138fb036b31fd26cafadaf3f9d0d2adba8 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 19 Feb 2022 23:33:46 +0100 Subject: [PATCH 036/237] add node view entity --- core/Entity/NodeView.php | 102 ++++++++++++++++++++ core/Factory/NodeViewFactory.php | 21 ++++ core/Repository/NodeViewRepository.php | 21 ++++ core/Repository/NodeViewRepositoryQuery.php | 27 ++++++ 4 files changed, 171 insertions(+) create mode 100644 core/Entity/NodeView.php create mode 100644 core/Factory/NodeViewFactory.php create mode 100644 core/Repository/NodeViewRepository.php create mode 100644 core/Repository/NodeViewRepositoryQuery.php diff --git a/core/Entity/NodeView.php b/core/Entity/NodeView.php new file mode 100644 index 0000000..506dbb5 --- /dev/null +++ b/core/Entity/NodeView.php @@ -0,0 +1,102 @@ +id; + } + + public function getNode(): ?Node + { + return $this->node; + } + + public function setNode(?Node $node): self + { + $this->node = $node; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function setPath(string $path): self + { + $this->path = $path; + + return $this; + } + + public function getViews(): ?int + { + return $this->views; + } + + public function setViews(int $views): self + { + $this->views = $views; + + return $this; + } + + public function addView(): self + { + ++$this->views; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } +} diff --git a/core/Factory/NodeViewFactory.php b/core/Factory/NodeViewFactory.php new file mode 100644 index 0000000..ecc3862 --- /dev/null +++ b/core/Factory/NodeViewFactory.php @@ -0,0 +1,21 @@ +setNode($node) + ->setPath($path) + ->setDate(new \DateTime()) + ; + + return $entity; + } +} diff --git a/core/Repository/NodeViewRepository.php b/core/Repository/NodeViewRepository.php new file mode 100644 index 0000000..5175e58 --- /dev/null +++ b/core/Repository/NodeViewRepository.php @@ -0,0 +1,21 @@ +andWhere('.node = :node') + ->andWhere('.path = :path') + ->setParameters([ + ':node' => $request->attributes->get('_node'), + ':path' => $request->getPathInfo(), + ]) + ; + } +} From 98456ab8e5752e452f3e7d95ed5bfc68897a42eb Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 19 Feb 2022 23:35:41 +0100 Subject: [PATCH 037/237] add node view listener --- config/services.yaml | 4 ++ core/EventListener/NodeViewListener.php | 68 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 core/EventListener/NodeViewListener.php diff --git a/config/services.yaml b/config/services.yaml index 88fe2f6..b81dbd3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,6 +23,10 @@ services: tags: - { name: kernel.event_listener, event: kernel.exception } + App\Core\EventListener\NodeViewListener: + tags: + - { name: kernel.event_listener, event: kernel.request } + App\: resource: '../src/' exclude: diff --git a/core/EventListener/NodeViewListener.php b/core/EventListener/NodeViewListener.php new file mode 100644 index 0000000..52b3880 --- /dev/null +++ b/core/EventListener/NodeViewListener.php @@ -0,0 +1,68 @@ + + */ +class NodeViewListener +{ + protected NodeRepository $nodeRepository; + protected NodeViewRepositoryQuery $nodeViewRepositoryQuery; + protected NodeViewFactory $nodeViewFactory; + protected EntityManager $manager; + + public function __construct( + NodeRepository $nodeRepository, + NodeViewRepositoryQuery $nodeViewRepositoryQuery, + NodeViewFactory $nodeViewFactory, + EntityManager $manager + ) { + $this->nodeRepository = $nodeRepository; + $this->nodeViewRepositoryQuery = $nodeViewRepositoryQuery; + $this->nodeViewFactory = $nodeViewFactory; + $this->manager = $manager; + } + + public function onKernelRequest(RequestEvent $event) + { + $request = $event->getRequest(); + + if (!$request->attributes->has('_node')) { + return; + } + + $node = $this->nodeRepository->findOneById($request->attributes->get('_node')); + + if (!$node || !$node->getEnableViewCounter()) { + return; + } + + $nodeView = $this->nodeViewRepositoryQuery->create() + ->filterByRequest($request) + ->andWhere('.date=CURRENT_DATE()') + ->findOne() + ; + + if (!$nodeView) { + $nodeView = $this->nodeViewFactory->create($node, $request->getPathInfo()); + } + + $nodeView->addView(); + + if ($nodeView->getId()) { + $this->manager->update($nodeView); + } else { + $this->manager->create($nodeView); + } + } +} From 58c8b6126ebb045fa2e1390d6732bedec90be9c4 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 13:08:54 +0100 Subject: [PATCH 038/237] remove useless class --- core/Site/Node.php | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 core/Site/Node.php diff --git a/core/Site/Node.php b/core/Site/Node.php deleted file mode 100644 index ec63344..0000000 --- a/core/Site/Node.php +++ /dev/null @@ -1,24 +0,0 @@ -id; - } -} From ec4b3341c87526d4efa087c164476376ca74467d Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 15:25:07 +0100 Subject: [PATCH 039/237] - move NodeView to Analytic/View - move NodeViewListener to AnalyticListener - add referer --- .gitignore | 2 + composer.json | 1 + config/services.yaml | 2 +- core/Entity/Analytic/Referer.php | 103 +++++++++++++++ .../{NodeView.php => Analytic/View.php} | 11 +- core/Entity/Site/Node.php | 68 +++++++--- core/EventListener/AnalyticListener.php | 124 ++++++++++++++++++ core/EventListener/NodeViewListener.php | 68 ---------- core/Factory/Analytic/RefererFactory.php | 22 ++++ .../ViewFactory.php} | 7 +- .../Repository/Analytic/RefererRepository.php | 21 +++ .../Analytic/RefererRepositoryQuery.php | 28 ++++ core/Repository/Analytic/ViewRepository.php | 21 +++ .../ViewRepositoryQuery.php} | 7 +- core/Repository/NodeViewRepository.php | 21 --- migrations/.gitignore | 0 symfony.lock | 3 + 17 files changed, 393 insertions(+), 116 deletions(-) create mode 100644 core/Entity/Analytic/Referer.php rename core/Entity/{NodeView.php => Analytic/View.php} (84%) create mode 100644 core/EventListener/AnalyticListener.php delete mode 100644 core/EventListener/NodeViewListener.php create mode 100644 core/Factory/Analytic/RefererFactory.php rename core/Factory/{NodeViewFactory.php => Analytic/ViewFactory.php} (64%) create mode 100644 core/Repository/Analytic/RefererRepository.php create mode 100644 core/Repository/Analytic/RefererRepositoryQuery.php create mode 100644 core/Repository/Analytic/ViewRepository.php rename core/Repository/{NodeViewRepositoryQuery.php => Analytic/ViewRepositoryQuery.php} (75%) delete mode 100644 core/Repository/NodeViewRepository.php delete mode 100644 migrations/.gitignore diff --git a/.gitignore b/.gitignore index 18ffc85..c0aa6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ yarn-error.log /public/uploads/ !/public/uploads/.gitkeep /public/media/ +/migrations/ +!/migrations/.gitkeep diff --git a/composer.json b/composer.json index 266ff76..dc97ae6 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^2.8", "friendsofsymfony/jsrouting-bundle": "^2.7", + "jaybizzle/crawler-detect": "^1.2", "knplabs/doctrine-behaviors": "^2.2", "knplabs/knp-paginator-bundle": "^5.8", "liip/imagine-bundle": "^2.6", diff --git a/config/services.yaml b/config/services.yaml index b81dbd3..4d04c70 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,7 +23,7 @@ services: tags: - { name: kernel.event_listener, event: kernel.exception } - App\Core\EventListener\NodeViewListener: + App\Core\EventListener\AnalyticListener: tags: - { name: kernel.event_listener, event: kernel.request } diff --git a/core/Entity/Analytic/Referer.php b/core/Entity/Analytic/Referer.php new file mode 100644 index 0000000..0281d3c --- /dev/null +++ b/core/Entity/Analytic/Referer.php @@ -0,0 +1,103 @@ +id; + } + + public function getNode(): ?Node + { + return $this->node; + } + + public function setNode(?Node $node): self + { + $this->node = $node; + + return $this; + } + + public function getUri(): ?string + { + return $this->uri; + } + + public function setUri(string $uri): self + { + $this->uri = $uri; + + return $this; + } + + public function getViews(): ?int + { + return $this->views; + } + + public function setViews(int $views): self + { + $this->views = $views; + + return $this; + } + + public function addView(): self + { + ++$this->views; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): self + { + $this->date = $date; + + return $this; + } +} diff --git a/core/Entity/NodeView.php b/core/Entity/Analytic/View.php similarity index 84% rename from core/Entity/NodeView.php rename to core/Entity/Analytic/View.php index 506dbb5..a5db1b9 100644 --- a/core/Entity/NodeView.php +++ b/core/Entity/Analytic/View.php @@ -1,16 +1,17 @@ children = new ArrayCollection(); $this->aliasNodes = new ArrayCollection(); - $this->nodeViews = new ArrayCollection(); + $this->analyticViews = new ArrayCollection(); + $this->analyticReferers = new ArrayCollection(); } public function getId(): ?int @@ -574,29 +582,59 @@ class Node implements EntityInterface } /** - * @return Collection|NodeView[] + * @return Collection|View[] */ - public function getNodeViews(): Collection + public function getAnalyticViews(): Collection { - return $this->nodeViews; + return $this->analyticViews; } - public function addNodeView(NodeView $nodeView): self + public function addAnalyticView(View $view): self { - if (!$this->nodeViews->contains($nodeView)) { - $this->nodeViews[] = $nodeView; - $nodeView->setNode($this); + if (!$this->analyticViews->contains($view)) { + $this->analyticViews[] = $view; + $view->setNode($this); } return $this; } - public function removeNodeView(NodeView $nodeView): self + public function removeAnalyticView(View $view): self { - if ($this->nodeViews->removeElement($nodeView)) { + if ($this->analyticViews->removeElement($view)) { // set the owning side to null (unless already changed) - if ($nodeView->getNode() === $this) { - $nodeView->setNode(null); + if ($view->getNode() === $this) { + $view->setNode(null); + } + } + + return $this; + } + + /** + * @return Collection|Referer[] + */ + public function getAnalyticReferers(): Collection + { + return $this->analyticReferers; + } + + public function addAnalyticReferer(Referer $referer): self + { + if (!$this->analyticReferers->contains($referer)) { + $this->analyticReferers[] = $referer; + $referer->setNode($this); + } + + return $this; + } + + public function removeAnalyticReferer(Referer $referer): self + { + if ($this->analyticReferers->removeElement($referer)) { + // set the owning side to null (unless already changed) + if ($referer->getNode() === $this) { + $referer->setNode(null); } } diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php new file mode 100644 index 0000000..eb6b983 --- /dev/null +++ b/core/EventListener/AnalyticListener.php @@ -0,0 +1,124 @@ + + */ +class AnalyticListener +{ + protected NodeRepository $nodeRepository; + protected ViewRepositoryQuery $viewRepositoryQuery; + protected ViewFactory $viewFactory; + protected RefererRepositoryQuery $refererRepositoryQuery; + protected RefererFactory $refererFactory; + protected EntityManager $manager; + protected CrawlerDetect $crawlerDetect; + protected Request $request; + protected Node $node; + + public function __construct( + NodeRepository $nodeRepository, + ViewRepositoryQuery $viewRepositoryQuery, + ViewFactory $viewFactory, + RefererRepositoryQuery $refererRepositoryQuery, + RefererFactory $refererFactory, + EntityManager $manager + ) { + $this->nodeRepository = $nodeRepository; + $this->viewRepositoryQuery = $viewRepositoryQuery; + $this->viewFactory = $viewFactory; + $this->refererRepositoryQuery = $refererRepositoryQuery; + $this->refererFactory = $refererFactory; + $this->manager = $manager; + $this->crawlerDetect = new CrawlerDetect(); + } + + public function onKernelRequest(RequestEvent $event) + { + $request = $event->getRequest(); + + if (!$request->attributes->has('_node')) { + return; + } + + if ($this->crawlerDetect->isCrawler($request->headers->get('user-agent'))) { + return; + } + + $node = $this->nodeRepository->findOneBy([ + 'id' => $request->attributes->get('_node'), + 'enableViewCounter' => true, + ]); + + if (!$node) { + return; + } + + $this->node = $node; + $this->request = $request; + + $this->createView(); + $this->createReferer(); + } + + protected function createView() + { + $entity = $this->viewRepositoryQuery->create() + ->filterByRequest($this->request) + ->andWhere('.date=CURRENT_DATE()') + ->findOne() + ; + + if (!$entity) { + $entity = $this->viewFactory->create($this->node, $this->request->getPathInfo()); + } + + $entity->addView(); + $this->save($entity); + } + + protected function createReferer() + { + if (!$this->request->headers->has('referer')) { + return; + } + + $entity = $this->refererRepositoryQuery->create() + ->filterByRequest($this->request) + ->andWhere('.date=CURRENT_DATE()') + ->findOne() + ; + + if (!$entity) { + $entity = $this->refererFactory->create($this->node, $this->request->headers->get('referer')); + } + + $entity->addView(); + $this->save($entity); + } + + protected function save(EntityInterface $entity) + { + if ($entity->getId()) { + $this->manager->update($entity); + } else { + $this->manager->create($entity); + } + } +} diff --git a/core/EventListener/NodeViewListener.php b/core/EventListener/NodeViewListener.php deleted file mode 100644 index 52b3880..0000000 --- a/core/EventListener/NodeViewListener.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ -class NodeViewListener -{ - protected NodeRepository $nodeRepository; - protected NodeViewRepositoryQuery $nodeViewRepositoryQuery; - protected NodeViewFactory $nodeViewFactory; - protected EntityManager $manager; - - public function __construct( - NodeRepository $nodeRepository, - NodeViewRepositoryQuery $nodeViewRepositoryQuery, - NodeViewFactory $nodeViewFactory, - EntityManager $manager - ) { - $this->nodeRepository = $nodeRepository; - $this->nodeViewRepositoryQuery = $nodeViewRepositoryQuery; - $this->nodeViewFactory = $nodeViewFactory; - $this->manager = $manager; - } - - public function onKernelRequest(RequestEvent $event) - { - $request = $event->getRequest(); - - if (!$request->attributes->has('_node')) { - return; - } - - $node = $this->nodeRepository->findOneById($request->attributes->get('_node')); - - if (!$node || !$node->getEnableViewCounter()) { - return; - } - - $nodeView = $this->nodeViewRepositoryQuery->create() - ->filterByRequest($request) - ->andWhere('.date=CURRENT_DATE()') - ->findOne() - ; - - if (!$nodeView) { - $nodeView = $this->nodeViewFactory->create($node, $request->getPathInfo()); - } - - $nodeView->addView(); - - if ($nodeView->getId()) { - $this->manager->update($nodeView); - } else { - $this->manager->create($nodeView); - } - } -} diff --git a/core/Factory/Analytic/RefererFactory.php b/core/Factory/Analytic/RefererFactory.php new file mode 100644 index 0000000..8884338 --- /dev/null +++ b/core/Factory/Analytic/RefererFactory.php @@ -0,0 +1,22 @@ +setNode($node) + ->setUri($uri) + ->setDate(new \DateTime()) + ; + + return $entity; + } +} diff --git a/core/Factory/NodeViewFactory.php b/core/Factory/Analytic/ViewFactory.php similarity index 64% rename from core/Factory/NodeViewFactory.php rename to core/Factory/Analytic/ViewFactory.php index ecc3862..840c7ad 100644 --- a/core/Factory/NodeViewFactory.php +++ b/core/Factory/Analytic/ViewFactory.php @@ -1,11 +1,12 @@ andWhere('.node = :node') + ->andWhere('.uri = :uri') + ->setParameters([ + ':node' => $request->attributes->get('_node'), + ':uri' => $request->headers->get('referer'), + ]) + ; + } +} diff --git a/core/Repository/Analytic/ViewRepository.php b/core/Repository/Analytic/ViewRepository.php new file mode 100644 index 0000000..4ddf0a3 --- /dev/null +++ b/core/Repository/Analytic/ViewRepository.php @@ -0,0 +1,21 @@ + Date: Sun, 20 Feb 2022 16:12:22 +0100 Subject: [PATCH 040/237] rename enableViewCounter with enableAnalytic --- core/Entity/Site/Node.php | 10 +++++----- core/Form/Site/NodeType.php | 4 ++-- core/Resources/translations/messages.fr.yaml | 3 +-- core/Resources/views/site/node_admin/_form.html.twig | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/core/Entity/Site/Node.php b/core/Entity/Site/Node.php index bfe78da..35a684e 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 $enableViewCounter = false; + protected $enableAnalytic = false; /** * @ORM\OneToMany(targetEntity=View::class, mappedBy="node") @@ -569,14 +569,14 @@ class Node implements EntityInterface return $this; } - public function getEnableViewCounter(): ?bool + public function getEnableAnalytic(): ?bool { - return $this->enableViewCounter; + return $this->enableAnalytic; } - public function setEnableViewCounter(bool $enableViewCounter): self + public function setEnableAnalytic(bool $enableAnalytic): self { - $this->enableViewCounter = $enableViewCounter; + $this->enableAnalytic = $enableAnalytic; return $this; } diff --git a/core/Form/Site/NodeType.php b/core/Form/Site/NodeType.php index 184cf55..4f41c1d 100644 --- a/core/Form/Site/NodeType.php +++ b/core/Form/Site/NodeType.php @@ -61,10 +61,10 @@ class NodeType extends AbstractType ); $builder->add( - 'enableViewCounter', + 'enableAnalytic', CheckboxType::class, [ - 'label' => 'Enable view counter', + 'label' => 'Enable analytic', 'required' => false, 'attr' => [ ], diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml index 26a9430..7f02595 100644 --- a/core/Resources/translations/messages.fr.yaml +++ b/core/Resources/translations/messages.fr.yaml @@ -39,8 +39,7 @@ "Never": "Jamais" "URL": "URL" "Disable URL": "Désactiver l'URL" -"Enable view counter": "Activer le compteur de vues" -"Stats": "Statistiques" +"Enable analytic": "Activer l'analyse" "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/site/node_admin/_form.html.twig b/core/Resources/views/site/node_admin/_form.html.twig index b1d7d78..44064ce 100644 --- a/core/Resources/views/site/node_admin/_form.html.twig +++ b/core/Resources/views/site/node_admin/_form.html.twig @@ -205,7 +205,7 @@ {{ form_row(form.disableUrl) }} - {{ form_row(form.enableViewCounter) }} + {{ form_row(form.enableAnalytic) }} {{ form_row(form.code) }} {{ form_row(form.contentType) }} {{ form_row(form.controller) }} From 23ee09b1b5e658e2fdacb3882e3b9035efcc6dd0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 21:05:18 +0100 Subject: [PATCH 041/237] NodeAdminController now extends AbstractController --- core/Controller/Site/NodeAdminController.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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, From e67966df4518e2271badbb1f6ef16fa63d5a96c6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 21:05:46 +0100 Subject: [PATCH 042/237] add analytic chart and table --- core/Analytic/RangeAnalytic.php | 133 ++++++++++++++++++ .../Analytic/AnalyticController.php | 37 +++++ core/Entity/Analytic/Referer.php | 10 +- core/Entity/Analytic/View.php | 10 +- core/Entity/Site/Node.php | 10 +- core/EventListener/AnalyticListener.php | 2 +- core/Form/Site/NodeType.php | 4 +- core/Resources/translations/messages.fr.yaml | 3 +- core/Resources/views/analytic/stats.html.twig | 111 +++++++++++++++ .../site/tree_admin/navigation.html.twig | 5 + 10 files changed, 306 insertions(+), 19 deletions(-) create mode 100644 core/Analytic/RangeAnalytic.php create mode 100644 core/Controller/Analytic/AnalyticController.php create mode 100644 core/Resources/views/analytic/stats.html.twig 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/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 @@ + 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 %} + + From 9d2048094ff5e161a66b49fcb61547cb106ba58e Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 21:05:53 +0100 Subject: [PATCH 043/237] add chartjs --- package.json | 1 + yarn.lock | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 65c9408..25ed802 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,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", "jquery": "^3.6.0", diff --git a/yarn.lock b/yarn.lock index fae2b28..6faa9fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1819,6 +1819,11 @@ chalk@^4.0.0: 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" @@ -2048,9 +2053,9 @@ core-js-compat@^3.8.1, core-js-compat@^3.9.0: semver "7.0.0" core-js@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" - integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" + integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== core-util-is@~1.0.0: version "1.0.2" From 690e117b0b6f2a010f953de6cd6b2270e6a18a5d Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 21:08:14 +0100 Subject: [PATCH 044/237] trigger events when modal content is reloaded --- assets/js/admin/modules/modal.js | 6 ++++++ 1 file changed, 6 insertions(+) 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 = $(' {{ form_row(form.disableUrl) }} - {{ form_row(form.enableAnalytic) }} + {{ form_row(form.enableAnalytics) }} {{ form_row(form.code) }} {{ form_row(form.contentType) }} {{ form_row(form.controller) }} diff --git a/core/Resources/views/site/tree_admin/navigation.html.twig b/core/Resources/views/site/tree_admin/navigation.html.twig index f49a211..31efad1 100644 --- a/core/Resources/views/site/tree_admin/navigation.html.twig +++ b/core/Resources/views/site/tree_admin/navigation.html.twig @@ -148,9 +148,11 @@ {% endif %} - + {% if node.enableAnalytics %} + + {% endif %} + {% if node.enableAnalytics %} + + {% endif %} + From 67694ffe8c6a7e1c482a1e717b08f8ac586aa4a0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 23:04:44 +0100 Subject: [PATCH 048/237] add no result when views and referers are empty --- core/Resources/views/analytic/stats.html.twig | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/Resources/views/analytic/stats.html.twig b/core/Resources/views/analytic/stats.html.twig index b1bf18d..8886d4a 100644 --- a/core/Resources/views/analytic/stats.html.twig +++ b/core/Resources/views/analytic/stats.html.twig @@ -56,6 +56,17 @@ {{ path }} {{ views }} + {% else %} + + +
+ +
+
+ {{ 'No result'|trans }} +
+ + {% endfor %} @@ -96,6 +107,17 @@ + {% else %} + + +
+ +
+
+ {{ 'No result'|trans }} +
+ + {% endfor %} From 914fef625a2f72a8e19b03bdb4dea5f3d937e28e Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 23:06:39 +0100 Subject: [PATCH 049/237] update translations --- core/Resources/translations/messages.fr.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml index 039b2cc..79033d2 100644 --- a/core/Resources/translations/messages.fr.yaml +++ b/core/Resources/translations/messages.fr.yaml @@ -41,6 +41,8 @@ "Disable URL": "Désactiver l'URL" "Enable analytics": "Activer les analyses" "Analytics": "Analyses" +"Referers": "Référents" +"Views": "Vues" "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" From 6c75f8ffc3c2aa7f907a01eb6f687f28ab6133b2 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sun, 20 Feb 2022 23:08:27 +0100 Subject: [PATCH 050/237] update translations --- core/Resources/translations/messages.fr.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/Resources/translations/messages.fr.yaml b/core/Resources/translations/messages.fr.yaml index 79033d2..83e5f44 100644 --- a/core/Resources/translations/messages.fr.yaml +++ b/core/Resources/translations/messages.fr.yaml @@ -43,6 +43,10 @@ "Analytics": "Analyses" "Referers": "Référents" "Views": "Vues" +"Last 7 days": "Les 7 derniers jours" +"Last 30 days": "Les 30 derniers jours" +"Last 90 days": "Les 90 derniers jours" +"Last year": "L'année passée" "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" From 0739f683c7ef01da9e7237899e1a2856aa9a08b0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 09:09:57 +0100 Subject: [PATCH 051/237] refactoring of RangeAnalytic --- core/Analytic/RangeAnalytic.php | 127 +++++++++++------- .../Analytic/AnalyticController.php | 17 +-- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/core/Analytic/RangeAnalytic.php b/core/Analytic/RangeAnalytic.php index a4d5ef6..c4a0f92 100644 --- a/core/Analytic/RangeAnalytic.php +++ b/core/Analytic/RangeAnalytic.php @@ -2,9 +2,9 @@ namespace App\Core\Analytic; +use App\Core\Entity\Site\Node; use App\Core\Repository\Analytic\RefererRepositoryQuery; use App\Core\Repository\Analytic\ViewRepositoryQuery; -use App\Core\Entity\Site\Node; /** * class RangeAnalytic. @@ -15,6 +15,11 @@ class RangeAnalytic { protected ViewRepositoryQuery $viewQuery; protected RefererRepositoryQuery $refererQuery; + protected ?Node $node; + protected ?\DateTime $from; + protected ?\DateTime $to; + protected bool $reload = true; + protected array $cache = []; public function __construct(ViewRepositoryQuery $viewQuery, RefererRepositoryQuery $refererQuery) { @@ -22,27 +27,22 @@ class RangeAnalytic $this->refererQuery = $refererQuery; } - public function getViews(\DateTime $from, \DateTime $to, Node $node): array + public function getViews(): 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() - ; + $entities = $this->getEntities('view'); + $this->reload = false; - $diff = $from->diff($to); + if ($entities) { + $first = $entities[0]; + $last = $entities[count($entities) - 1]; - if ($diff->days >= 365) { - $format = 'Y-m'; - } else { - $format = 'Y-m-d'; + $diff = $first->getDate()->diff($last->getDate()); + + if ($diff->days >= 90) { + $format = 'Y-m'; + } else { + $format = 'Y-m-d'; + } } $datas = []; @@ -60,20 +60,10 @@ class RangeAnalytic return $datas; } - public function getPathViews(\DateTime $from, \DateTime $to, Node $node): array + public function getPathViews(): 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() - ; + $entities = $this->getEntities('view'); + $this->reload = false; $datas = []; @@ -90,20 +80,10 @@ class RangeAnalytic return $datas; } - public function getReferers(\DateTime $from, \DateTime $to, Node $node): array + public function getReferers(): 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() - ; + $entities = $this->getEntities('referer'); + $this->reload = false; $datas = []; @@ -130,4 +110,61 @@ class RangeAnalytic return $datas; } + + public function setDateRange(?\DateTime $from, ?\DateTime $to): self + { + $this->from = $from; + $this->to = $to; + $this->reload = true; + + return $this; + } + + public function setNode(?Node $node): self + { + $this->node = $node; + $this->reload = true; + + return $this; + } + + protected function getEntities(string $type): array + { + if ('view' === $type) { + $query = $this->viewQuery->create(); + } elseif ('referer' === $type) { + $query = $this->refererQuery->create(); + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + if (!$this->reload && isset($this->cache[$type])) { + return $this->cache[$type]; + } + + if (null !== $this->from) { + $query + ->andWhere('.date >= :from') + ->setParameter(':from', $this->from) + ; + } + + if (null !== $this->to) { + $query + ->andWhere('.date <= :to') + ->setParameter(':to', $this->to) + ; + } + + if (null !== $this->node) { + $query + ->andWhere('.node = :node') + ->setParameter(':node', $this->node->getId()) + ; + } + + $this->cache[$type] = $query->orderBy('.date')->find(); + + return $this->cache[$type]; + } } diff --git a/core/Controller/Analytic/AnalyticController.php b/core/Controller/Analytic/AnalyticController.php index 83b9640..8a0156b 100644 --- a/core/Controller/Analytic/AnalyticController.php +++ b/core/Controller/Analytic/AnalyticController.php @@ -2,11 +2,11 @@ namespace App\Core\Controller\Analytic; +use App\Core\Analytic\RangeAnalytic; +use App\Core\Entity\Site\Node; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -use App\Core\Entity\Site\Node; -use App\Core\Analytic\RangeAnalytic; /** * @Route("/admin/analytic") @@ -22,15 +22,16 @@ class AnalyticController extends AbstractController throw $this->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); + $rangeAnalytic + ->setDateRange(new \DateTime('now - '.$range), new \DateTime()) + ->setNode($node) + ; return $this->render('@Core/analytic/stats.html.twig', [ 'range' => $range, - 'views' => $views, - 'pathViews' => $pathViews, - 'referers' => $referers, + 'views' => $rangeAnalytic->getViews(), + 'pathViews' => $rangeAnalytic->getPathViews(), + 'referers' => $rangeAnalytic->getReferers(), 'node' => $node, ]); } From 5810e09a4516b2126eed3a1247d2d9a81fe9af51 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:12:05 +0100 Subject: [PATCH 052/237] add sort of datas by view in DateRangeAnalytic --- ...angeAnalytic.php => DateRangeAnalytic.php} | 20 +++++++++++++++++-- .../Analytic/AnalyticController.php | 12 +++++------ core/EventListener/AnalyticListener.php | 12 ++++++++++- 3 files changed, 35 insertions(+), 9 deletions(-) rename core/Analytic/{RangeAnalytic.php => DateRangeAnalytic.php} (91%) diff --git a/core/Analytic/RangeAnalytic.php b/core/Analytic/DateRangeAnalytic.php similarity index 91% rename from core/Analytic/RangeAnalytic.php rename to core/Analytic/DateRangeAnalytic.php index c4a0f92..ce4a60f 100644 --- a/core/Analytic/RangeAnalytic.php +++ b/core/Analytic/DateRangeAnalytic.php @@ -7,11 +7,11 @@ use App\Core\Repository\Analytic\RefererRepositoryQuery; use App\Core\Repository\Analytic\ViewRepositoryQuery; /** - * class RangeAnalytic. + * class DateRangeAnalytic. * * @author Simon Vieille */ -class RangeAnalytic +class DateRangeAnalytic { protected ViewRepositoryQuery $viewQuery; protected RefererRepositoryQuery $refererQuery; @@ -57,6 +57,8 @@ class RangeAnalytic $datas[$index] += $entity->getViews(); } + arsort($datas, SORT_NUMERIC); + return $datas; } @@ -77,6 +79,8 @@ class RangeAnalytic $datas[$index] += $entity->getViews(); } + arsort($datas, SORT_NUMERIC); + return $datas; } @@ -108,6 +112,18 @@ class RangeAnalytic $datas[$index]['uris'][$path] += $entity->getViews(); } + uasort($datas, function($a, $b) { + if ($a['views'] > $b['views']) { + return -1; + } + + if ($a['views'] < $b['views']) { + return 1; + } + + return 0; + }); + return $datas; } diff --git a/core/Controller/Analytic/AnalyticController.php b/core/Controller/Analytic/AnalyticController.php index 8a0156b..6d2fd88 100644 --- a/core/Controller/Analytic/AnalyticController.php +++ b/core/Controller/Analytic/AnalyticController.php @@ -2,7 +2,7 @@ namespace App\Core\Controller\Analytic; -use App\Core\Analytic\RangeAnalytic; +use App\Core\Analytic\DateRangeAnalytic; use App\Core\Entity\Site\Node; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -16,22 +16,22 @@ class AnalyticController extends AbstractController /** * @Route("/stats/{node}/{range}", name="admin_analytic_stats") */ - public function stats(Node $node, RangeAnalytic $rangeAnalytic, string $range = '7days'): Response + public function stats(Node $node, DateRangeAnalytic $analytic, string $range = '7days'): Response { if (!in_array($range, ['7days', '30days', '90days', '1year'])) { throw $this->createNotFoundException(); } - $rangeAnalytic + $analytic ->setDateRange(new \DateTime('now - '.$range), new \DateTime()) ->setNode($node) ; return $this->render('@Core/analytic/stats.html.twig', [ 'range' => $range, - 'views' => $rangeAnalytic->getViews(), - 'pathViews' => $rangeAnalytic->getPathViews(), - 'referers' => $rangeAnalytic->getReferers(), + 'views' => $analytic->getViews(), + 'pathViews' => $analytic->getPathViews(), + 'referers' => $analytic->getReferers(), 'node' => $node, ]); } diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php index 6154378..515e39d 100644 --- a/core/EventListener/AnalyticListener.php +++ b/core/EventListener/AnalyticListener.php @@ -99,6 +99,16 @@ class AnalyticListener return; } + $referer = $this->request->headers->get('referer'); + + if (!filter_var($referer, FILTER_VALIDATE_URL) || parse_url($url, PHP_URL_SCHEME)) { + return; + } + + if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'])) { + return; + } + $entity = $this->refererRepositoryQuery->create() ->filterByRequest($this->request) ->andWhere('.date=CURRENT_DATE()') @@ -106,7 +116,7 @@ class AnalyticListener ; if (!$entity) { - $entity = $this->refererFactory->create($this->node, $this->request->headers->get('referer')); + $entity = $this->refererFactory->create($this->node, $referer); } $entity->addView(); From da77abbf3cdd3ef21e1e44f7455024e53f251245 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:15:22 +0100 Subject: [PATCH 053/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfee651..5a6e9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added +* add basic analytics ### Fixed ### Changed From 621ce7a82b7866607f5869eb36608eb5293143ca Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:15:35 +0100 Subject: [PATCH 054/237] add UPGRADE.md file --- UPGRADE.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 UPGRADE.md diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..593b2d1 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,6 @@ +## [Unreleased] + +``` +yarn add chart.js --save +composer require jaybizzle/crawler-detect +``` From 3858546e4e6cb8efb3f43f0618bbdb095b8f49d9 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:40:01 +0100 Subject: [PATCH 055/237] fix analytics listener --- core/EventListener/AnalyticListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php index 515e39d..8f6506e 100644 --- a/core/EventListener/AnalyticListener.php +++ b/core/EventListener/AnalyticListener.php @@ -101,7 +101,7 @@ class AnalyticListener $referer = $this->request->headers->get('referer'); - if (!filter_var($referer, FILTER_VALIDATE_URL) || parse_url($url, PHP_URL_SCHEME)) { + if (!filter_var($referer, FILTER_VALIDATE_URL)) { return; } From 1d8270ddae1932d0d36000807715eb2bc2681f72 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:40:15 +0100 Subject: [PATCH 056/237] fix analytics listener --- core/EventListener/AnalyticListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php index 8f6506e..fdec2f5 100644 --- a/core/EventListener/AnalyticListener.php +++ b/core/EventListener/AnalyticListener.php @@ -105,7 +105,7 @@ class AnalyticListener return; } - if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'])) { + if (!in_array(parse_url($referer, PHP_URL_SCHEME), ['http', 'https'])) { return; } From f660f1282542468af5a23e05c6b49a7a1eb9a7e5 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 16:40:28 +0100 Subject: [PATCH 057/237] update upgrade doc --- UPGRADE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 593b2d1..064e9e6 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,6 +1,27 @@ ## [Unreleased] + +### Commands + ``` +yarn remove node-sass +yarn add sass --dev --save yarn add chart.js --save composer require jaybizzle/crawler-detect +make doctrine-migration +make asset +``` + +### Configuration + +``` +// config/services.yaml +services: + App\Core\EventListener\RedirectListener: + tags: + - { name: kernel.event_listener, event: kernel.exception } + + App\Core\EventListener\AnalyticListener: + tags: + - { name: kernel.event_listener, event: kernel.request } ``` From 901f5ba25ea7ec95c575101159663a64f62655d0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 19:37:15 +0100 Subject: [PATCH 058/237] update makefile rules: using npm binary instead of webpack binary --- Makefile | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 3ad92f2..015e70b 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,20 @@ COMPOSER_BIN ?= composer PHP_BIN ?= php8.1 SSH_BIN ?= ssh -WEBPACK_BIN ?= webpack YARN_BIN ?= yarn +NPM_BIN ?= npm -all: dep asset clean - -.ONESHELL: -dep: - $(COMPOSER_BIN) update --ignore-platform-reqs - $(COMPOSER_BIN) install --ignore-platform-reqs - $(YARN_BIN) +all: build asset-watch: - $(WEBPACK_BIN) -w + $(YARN_BIN) + $(NPM_BIN) run watch asset: js-routing $(YARN_BIN) - $(WEBPACK_BIN) + $(NPM_BIN) run build -js-routing: +js-routing: doctrine-migration $(PHP_BIN) bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json clean: @@ -28,3 +23,5 @@ clean: doctrine-migration: PHP=$(PHP_BIN) ./bin/doctrine-migrate + +build: js-routing asset From 933888f0615ab8ebc2b5e565abfd321eb629bf6c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 19:38:00 +0100 Subject: [PATCH 059/237] update upgrade doc --- UPGRADE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPGRADE.md b/UPGRADE.md index 064e9e6..c8fb1a0 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,6 @@ ## [Unreleased] +## Upgrade to v1.4.0 ### Commands From a6f35fa38e141eb53da593a687e64737543f3d66 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 19:44:37 +0100 Subject: [PATCH 060/237] update makefile rules --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 015e70b..8b61d9b 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,4 @@ clean: doctrine-migration: PHP=$(PHP_BIN) ./bin/doctrine-migrate -build: js-routing asset +build: clean js-routing asset From e50d2668f9dc7b928e30467dc4cb4efa81995ee0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 19:45:25 +0100 Subject: [PATCH 061/237] update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6e9c6..8bdb477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ ## [Unreleased] ### Added -* add basic analytics ### Fixed ### Changed +## 1.4.0 +### Added +* add basic analytics + ## 1.3.0 ### Added * add support of regexp with substitution in redirect From d7659735cb53d846b01ec8e57fb6982b69655b56 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 23:58:08 +0100 Subject: [PATCH 062/237] update default template --- assets/css/_admin_extend.scss | 6 +- assets/css/app.scss | 0 assets/js/app.js | 1 + templates/base.html.twig | 67 ++++++++++++--- templates/page/simple/default.html.twig | 103 +++--------------------- webpack.config.js | 1 + 6 files changed, 68 insertions(+), 110 deletions(-) create mode 100644 assets/css/app.scss create mode 100644 assets/js/app.js diff --git a/assets/css/_admin_extend.scss b/assets/css/_admin_extend.scss index 88f6b3d..8b13789 100644 --- a/assets/css/_admin_extend.scss +++ b/assets/css/_admin_extend.scss @@ -1,5 +1 @@ -$theme-colors: ( - "primary": #1ab5dc, - "primary-light": lighten(#3183aa, 40%), - "dark-blue": #1e2430, -) !default; + diff --git a/assets/css/app.scss b/assets/css/app.scss new file mode 100644 index 0000000..e69de29 diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..0fc863e --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1 @@ +import '../css/app.scss' diff --git a/templates/base.html.twig b/templates/base.html.twig index 16d7273..fa9961f 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,19 +1,62 @@ +{% apply spaceless %} - + - - {% block title %}Welcome!{% endblock %} - {# Run `composer require symfony/webpack-encore-bundle` - and uncomment the following Encore helpers to start using Symfony UX #} - {% block stylesheets %} - {#{{ encore_entry_link_tags('app') }}#} - {% endblock %} + + - {% block javascripts %} - {#{{ encore_entry_script_tags('app') }}#} - {% endblock %} + + + {%- block opengraph -%} + + + + + + {%- if _page.ogImage -%} + + {%- endif -%} + {%- endblock -%} + + {%- block stylesheets -%} + {{ encore_entry_link_tags('app') }} + {%- endblock -%} + + {{ _page.metaTitle }} + - {% block body %}{% endblock %} + {# + - The current node is `_node` and its menu is `_menu` + - The current navigation is `_navigation` + - The current locale is `_locale` + + - Retrieve all navigations: `_store.navigations` + - Retrieve a navigation by its code: `_store.navigation('the_code')` + + - Retrieve all navigation menus: `_navigation.menus` + - Retrieve a menu by its code: `_navigation.menu('the_code')` + - Retrieve all nodes of a menu: `menu.rootNode.children` + - Retrieve visible nodes of a menu: `menu.rootNode.children({visible: true})` + + - Test if a node is the current one: `_store.isActiveNode(node)` + - Test if a node is or contains the current one: `_store.isActiveNode(node, true)` + - Generate a node url: + ``` + {% if node.hasExternalUrl or node.hasAppUrl %} + {% set url = node.url %} + {% else %} + {% set url = safe_node_path(node) %} + {% endif %} + ``` + - Generate a node url when the navigation has several domains: `safe_node_path(node, {_domain: _domain})` + #} + + {% block page %}{% endblock %} + + {%- block javascripts -%} + {{ encore_entry_script_tags('app') }} + {%- endblock -%} +{% endapply %} diff --git a/templates/page/simple/default.html.twig b/templates/page/simple/default.html.twig index a737bca..0e314d8 100644 --- a/templates/page/simple/default.html.twig +++ b/templates/page/simple/default.html.twig @@ -1,99 +1,16 @@ -{% import _self as macros %} +{% extends 'base.html.twig' %} -{% macro item(node, store) %} - {% set isActive = store.isActiveNode(node, true) %} +{%- block page -%} +

{{ _page.title.value }}

- {% if node.isVisible %} - {% if node.code == 'post' %} - - {{ node.label }} - - {% elseif node.page %} - {% set url = node.hasExternalUrl ? node.url : url(node.routeName) %} + {{ _page.content.value|murph_url|file_attributes|raw }} - - {{ node.label }} - - {% else %} - - {{ node.label }} - - {% endif %} +
- {% if node.children|length %} -
    - {% for child in node.children %} - {% if child.isVisible %} -
  • - {{ macros.item(child, store) }} -
  • - {% endif %} - {% endfor %} -
- {% endif %} + {% set image = _page.image.value %} + + {% if image %} + {{ image|file_attribute('alt') }} {% endif %} -{% endmacro %} +{%- endblock -%} -{% macro menu(menu, store) %} -
    - {% for child in menu.rootNode.children %} - {% if child.isVisible %} -
  • - {{ macros.item(child, store) }} -
  • - {% endif %} - {% endfor %} -
-{% endmacro %} - -

{{ _page.title.value }}

- -
{{ _page.content.value|murph_url|file_attributes }}
- -{% set image = _page.image.value %} - -{% if image %} - -{% endif %} - -

Request

- -
    -
  • - Node: {{ _node.label }} -
  • -
  • - Menu: {{ _menu.label }} -
  • -
  • - Navigation: {{ _navigation.label }} -
  • -
  • - Locale: - -
      -
    • - Var: {{ _locale }} -
    • -
    • - Navigation: {{ _navigation.locale }} -
    • -
    • - Request: {{ app.request.attributes.get('_locale') }} -
    • -
    • - Test: {{ 'This is a test of translation'|trans }} -
    • -
    -
  • -
- -

Menus

- -{# {% set menu = _navigation.menu('top') %} #} - -{% for menu in _navigation.menus %} -

{{ menu.label }} / {{ menu.code }}

- - {{ macros.menu(menu, _store) }} -{% endfor %} diff --git a/webpack.config.js b/webpack.config.js index 6ceaea5..a61b3a7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ Encore * and one CSS file (e.g. app.css) if your JavaScript imports CSS. */ .addEntry('admin', './assets/js/admin.js') + .addEntry('app', './assets/js/app.js') // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. .splitEntryChunks() From 5129e70f8bd1b483790fd5c9f52ecb265ac607f3 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 21 Feb 2022 23:58:40 +0100 Subject: [PATCH 063/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bdb477..a757a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added ### Fixed ### Changed +* update default templates ## 1.4.0 ### Added From d442d343bad41d816d70b29b9ea8272922d9ffe9 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 22 Feb 2022 00:01:13 +0100 Subject: [PATCH 064/237] update default logo --- assets/images/logo.png | Bin 32206 -> 0 bytes templates/base.html.twig | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 assets/images/logo.png diff --git a/assets/images/logo.png b/assets/images/logo.png deleted file mode 100644 index 53eae46cf3ab3e4650d8ed946265c640af6ebcd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32206 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 diff --git a/templates/base.html.twig b/templates/base.html.twig index fa9961f..1293d7d 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -5,7 +5,7 @@ - + {%- block opengraph -%} From a85aea5afbc37ea41bb757553a3afb8ebccea138 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Tue, 22 Feb 2022 14:16:34 +0100 Subject: [PATCH 065/237] fix views in analytics modal replace empty path with / in analytics --- CHANGELOG.md | 2 ++ core/Analytic/DateRangeAnalytic.php | 4 ++++ core/Resources/views/analytic/stats.html.twig | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a757a23..1dee78d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added ### Fixed +* fix views in analytics modal +* replace empty path with "/" in analytics ### Changed * update default templates diff --git a/core/Analytic/DateRangeAnalytic.php b/core/Analytic/DateRangeAnalytic.php index ce4a60f..36e818d 100644 --- a/core/Analytic/DateRangeAnalytic.php +++ b/core/Analytic/DateRangeAnalytic.php @@ -105,6 +105,10 @@ class DateRangeAnalytic $path = parse_url($entity->getUri(), PHP_URL_PATH); + if (empty($path)) { + $path = '/'; + } + if (!isset($datas[$index]['uris'][$path])) { $datas[$index]['uris'][$path] = 0; } diff --git a/core/Resources/views/analytic/stats.html.twig b/core/Resources/views/analytic/stats.html.twig index 8886d4a..8190fa1 100644 --- a/core/Resources/views/analytic/stats.html.twig +++ b/core/Resources/views/analytic/stats.html.twig @@ -98,7 +98,7 @@ {% for path, views in info.uris %}
- {{ info.views }} + {{ views }}
{{ path }} From 9d77d6ed6d02bdd1aef670b96abb94a3fb05f70c Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 10:58:52 +0100 Subject: [PATCH 066/237] handle app url in twig routing filters --- core/Twig/Extension/RoutingExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/Twig/Extension/RoutingExtension.php b/core/Twig/Extension/RoutingExtension.php index 287ea93..683153c 100644 --- a/core/Twig/Extension/RoutingExtension.php +++ b/core/Twig/Extension/RoutingExtension.php @@ -62,7 +62,7 @@ class RoutingExtension extends AbstractExtension public function getNodePath(Node $node, array $parameters = [], bool $relative = false): ?string { - if ($node->hasExternalUrl()) { + if ($node->hasExternalUrl() || $node->hasAppUrl()) { return $node->getUrl(); } @@ -75,7 +75,7 @@ class RoutingExtension extends AbstractExtension public function getNodeUrl(Node $node, array $parameters = [], bool $schemeRelative = false): ?string { - if ($node->hasExternalUrl()) { + if ($node->hasExternalUrl() || $node->hasAppUrl()) { return $node->getUrl(); } From e04f1199c4a751eefb82e8aa02787010e252dd06 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 10:59:02 +0100 Subject: [PATCH 067/237] update default template --- templates/base.html.twig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/templates/base.html.twig b/templates/base.html.twig index 1293d7d..c307dca 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -43,10 +43,11 @@ - Test if a node is or contains the current one: `_store.isActiveNode(node, true)` - Generate a node url: ``` - {% if node.hasExternalUrl or node.hasAppUrl %} - {% set url = node.url %} + {% if not node.disableUrl %} + {% set path = safe_node_path(node) %} + {% set url = safe_node_url(node) %} {% else %} - {% set url = safe_node_path(node) %} + {% set url = null %} {% endif %} ``` - Generate a node url when the navigation has several domains: `safe_node_path(node, {_domain: _domain})` From dad11586b22e64de5c0f8e904e219cef9de85f52 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 11:01:23 +0100 Subject: [PATCH 068/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dee78d..bf33d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added +* handle app urls in twig routing filters ### Fixed * fix views in analytics modal * replace empty path with "/" in analytics From 28b738d70d65e8249bc101af14f2454de8a87184 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 11:01:48 +0100 Subject: [PATCH 069/237] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf33d3a..297a751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## [Unreleased] +### Added +### Fixed +### Changed + +## 1.4.1 ### Added * handle app urls in twig routing filters ### Fixed From 07f2ea1d9e9e81936c946ae2e49a5692fe48128d Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 13:20:17 +0100 Subject: [PATCH 070/237] update dependencies --- composer.json | 30 +++++++++++++++--------------- symfony.lock | 24 ------------------------ 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/composer.json b/composer.json index dc97ae6..1c52ff1 100644 --- a/composer.json +++ b/composer.json @@ -9,25 +9,25 @@ "php": ">=8.0.0", "ext-ctype": "*", "ext-iconv": "*", - "bjeavons/zxcvbn-php": "^1.2", - "cocur/slugify": "^4.0", + "bjeavons/zxcvbn-php": "^1.3", + "cocur/slugify": "^4.1", "composer/package-versions-deprecated": "1.11.99.1", "doctrine/annotations": "^1.0", - "doctrine/doctrine-bundle": "^2.2", - "doctrine/doctrine-migrations-bundle": "^3.0", - "doctrine/orm": "^2.8", - "friendsofsymfony/jsrouting-bundle": "^2.7", + "doctrine/doctrine-bundle": "^2.5", + "doctrine/doctrine-migrations-bundle": "^3.2", + "doctrine/orm": "^2.11", + "friendsofsymfony/jsrouting-bundle": "^2.8", "jaybizzle/crawler-detect": "^1.2", - "knplabs/doctrine-behaviors": "^2.2", + "knplabs/doctrine-behaviors": "^2.6", "knplabs/knp-paginator-bundle": "^5.8", - "liip/imagine-bundle": "^2.6", - "phpdocumentor/reflection-docblock": "^5.2", - "scheb/2fa-google-authenticator": "^5.7", - "scheb/2fa-qr-code": "^5.7", - "sensio/framework-extra-bundle": "^6.1", + "liip/imagine-bundle": "^2.7", + "phpdocumentor/reflection-docblock": "^5.3", + "scheb/2fa-google-authenticator": "^5.13", + "scheb/2fa-qr-code": "^5.13", + "sensio/framework-extra-bundle": "^6.2", "sensiolabs/ansi-to-html": "^1.2", "spe/filesize-extension-bundle": "~2.0.0", - "stof/doctrine-extensions-bundle": "^1.6", + "stof/doctrine-extensions-bundle": "^1.7", "symfony/apache-pack": "^1.0", "symfony/asset": "5.4.*", "symfony/console": "5.4.*", @@ -57,8 +57,8 @@ "symfony/web-link": "5.4.*", "symfony/webpack-encore-bundle": "^1.11", "symfony/yaml": "5.4.*", - "twig/extra-bundle": "^2.12|^3.0", - "twig/twig": "^2.12|^3.0" + "twig/extra-bundle": "^2.12|^3.3", + "twig/twig": "^2.12|^3.3" }, "require-dev": { "symfony/browser-kit": "^5.2", diff --git a/symfony.lock b/symfony.lock index 58d5c8b..fb60411 100644 --- a/symfony.lock +++ b/symfony.lock @@ -147,12 +147,6 @@ "laminas/laminas-code": { "version": "4.0.0" }, - "laminas/laminas-eventmanager": { - "version": "3.3.1" - }, - "laminas/laminas-zendframework-bridge": { - "version": "1.2.0" - }, "liip/imagine-bundle": { "version": "1.8", "recipe": { @@ -326,9 +320,6 @@ "config/packages/dev/debug.yaml" ] }, - "symfony/debug-pack": { - "version": "v1.0.9" - }, "symfony/dependency-injection": { "version": "v5.2.5" }, @@ -469,9 +460,6 @@ "symfony/options-resolver": { "version": "v5.2.4" }, - "symfony/orm-pack": { - "version": "v2.1.0" - }, "symfony/password-hasher": { "version": "v6.0.2" }, @@ -517,9 +505,6 @@ "symfony/process": { "version": "v5.2.4" }, - "symfony/profiler-pack": { - "version": "v1.0.5" - }, "symfony/property-access": { "version": "v5.2.4" }, @@ -570,9 +555,6 @@ "symfony/serializer": { "version": "v5.2.4" }, - "symfony/serializer-pack": { - "version": "v1.0.4" - }, "symfony/service-contracts": { "version": "v2.2.0" }, @@ -582,9 +564,6 @@ "symfony/string": { "version": "v5.2.4" }, - "symfony/test-pack": { - "version": "v1.0.7" - }, "symfony/translation": { "version": "3.3", "recipe": { @@ -618,9 +597,6 @@ "templates/base.html.twig" ] }, - "symfony/twig-pack": { - "version": "v1.0.1" - }, "symfony/validator": { "version": "4.3", "recipe": { From bffb9aac591497121096c779b2301d65e31ffb7f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 13:31:37 +0100 Subject: [PATCH 071/237] update dependencies --- composer.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 1c52ff1..fee47b3 100644 --- a/composer.json +++ b/composer.json @@ -61,14 +61,14 @@ "twig/twig": "^2.12|^3.3" }, "require-dev": { - "symfony/browser-kit": "^5.2", - "symfony/css-selector": "^5.2", - "symfony/debug-bundle": "^5.2", + "symfony/browser-kit": "^5.4", + "symfony/css-selector": "^5.4", + "symfony/debug-bundle": "^5.4", "symfony/maker-bundle": "^1.0", - "symfony/phpunit-bridge": "^5.2", - "symfony/stopwatch": "^5.2", - "symfony/var-dumper": "^5.2", - "symfony/web-profiler-bundle": "^5.2" + "symfony/phpunit-bridge": "^5.4", + "symfony/stopwatch": "^5.4", + "symfony/var-dumper": "^5.4", + "symfony/web-profiler-bundle": "^5.4" }, "config": { "optimize-autoloader": true, From f29289a36d98e28f00a39f97fa088b46f10e09ed Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 23 Feb 2022 13:33:41 +0100 Subject: [PATCH 072/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 297a751..3fe294f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added +* update dependencies ### Fixed ### Changed From 4db7936a2caca399dcb9645461098cf9690d61e6 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 25 Feb 2022 14:36:44 +0100 Subject: [PATCH 073/237] update visibility of entities properties --- core/Entity/Redirect.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/Entity/Redirect.php b/core/Entity/Redirect.php index 97cbf9b..0b1bcb4 100644 --- a/core/Entity/Redirect.php +++ b/core/Entity/Redirect.php @@ -65,12 +65,12 @@ class Redirect implements EntityInterface /** * @ORM\Column(type="boolean") */ - private $isEnabled; + protected $isEnabled; /** * @ORM\Column(type="boolean") */ - private $reuseQueryString; + protected $reuseQueryString; public function getId(): ?int { From 33eec5204415ab2df1bd03b4557870856863a51f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 25 Feb 2022 15:15:09 +0100 Subject: [PATCH 074/237] add mobile and desktop views --- .php-version | 1 + UPGRADE.md | 8 +++ composer.json | 1 + core/Analytic/DateRangeAnalytic.php | 24 +++++++-- core/Entity/Analytic/Referer.php | 2 +- core/Entity/Analytic/View.php | 50 ++++++++++++++++++- core/EventListener/AnalyticListener.php | 47 +++++++++++------ core/Resources/views/analytic/stats.html.twig | 10 ++-- symfony.lock | 6 +++ 9 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 .php-version diff --git a/.php-version b/.php-version new file mode 100644 index 0000000..cc40bca --- /dev/null +++ b/.php-version @@ -0,0 +1 @@ +8.0 diff --git a/UPGRADE.md b/UPGRADE.md index c8fb1a0..6c79947 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,13 @@ ## [Unreleased] +### Commands + +``` +composer remove jaybizzle/crawler-detect +composer require matomo/device-detector +make doctrine-migration +``` + ## Upgrade to v1.4.0 ### Commands diff --git a/composer.json b/composer.json index fee47b3..1172189 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "knplabs/doctrine-behaviors": "^2.6", "knplabs/knp-paginator-bundle": "^5.8", "liip/imagine-bundle": "^2.7", + "matomo/device-detector": "^5.0", "phpdocumentor/reflection-docblock": "^5.3", "scheb/2fa-google-authenticator": "^5.13", "scheb/2fa-qr-code": "^5.13", diff --git a/core/Analytic/DateRangeAnalytic.php b/core/Analytic/DateRangeAnalytic.php index 36e818d..15f46ae 100644 --- a/core/Analytic/DateRangeAnalytic.php +++ b/core/Analytic/DateRangeAnalytic.php @@ -57,8 +57,6 @@ class DateRangeAnalytic $datas[$index] += $entity->getViews(); } - arsort($datas, SORT_NUMERIC); - return $datas; } @@ -73,13 +71,29 @@ class DateRangeAnalytic $index = $entity->getPath(); if (!isset($datas[$index])) { - $datas[$index] = 0; + $datas[$index] = [ + 'views' => 0, + 'desktopViews' => 0, + 'mobileViews' => 0, + ]; } - $datas[$index] += $entity->getViews(); + $datas[$index]['views'] += $entity->getViews(); + $datas[$index]['desktopViews'] += $entity->getDesktopViews(); + $datas[$index]['mobileViews'] += $entity->getMobileViews(); } - arsort($datas, SORT_NUMERIC); + uasort($datas, function($a, $b) { + if ($a['views'] > $b['views']) { + return -1; + } + + if ($a['views'] < $b['views']) { + return 1; + } + + return 0; + }); return $datas; } diff --git a/core/Entity/Analytic/Referer.php b/core/Entity/Analytic/Referer.php index 1c8b94d..0eeb73c 100644 --- a/core/Entity/Analytic/Referer.php +++ b/core/Entity/Analytic/Referer.php @@ -21,7 +21,7 @@ class Referer implements EntityInterface protected $id; /** - * @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews") + * @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticReferers") * @ORM\JoinColumn(nullable=false, onDelete="CASCADE") */ protected $node; diff --git a/core/Entity/Analytic/View.php b/core/Entity/Analytic/View.php index 0bd3338..25dd681 100644 --- a/core/Entity/Analytic/View.php +++ b/core/Entity/Analytic/View.php @@ -21,7 +21,7 @@ class View implements EntityInterface protected $id; /** - * @ORM\ManyToOne(targetEntity=Node::class, inversedBy="nodeViews") + * @ORM\ManyToOne(targetEntity=Node::class, inversedBy="analyticViews") * @ORM\JoinColumn(nullable=false, onDelete="CASCADE") */ protected $node; @@ -36,6 +36,16 @@ class View implements EntityInterface */ protected $views = 0; + /** + * @ORM\Column(type="integer", options={"default"=0}) + */ + protected $desktopViews = 0; + + /** + * @ORM\Column(type="integer", options={"default"=0}) + */ + protected $mobileViews = 0; + /** * @ORM\Column(type="date") */ @@ -89,6 +99,44 @@ class View implements EntityInterface return $this; } + public function getDesktopViews(): ?int + { + return $this->desktopViews; + } + + public function setDesktopViews(int $desktopViews): self + { + $this->desktopViews = $desktopViews; + + return $this; + } + + public function addDesktopView(): self + { + ++$this->desktopViews; + + return $this; + } + + public function getMobileViews(): ?int + { + return $this->mobileViews; + } + + public function setMobileViews(int $mobileViews): self + { + $this->mobileViews = $mobileViews; + + return $this; + } + + public function addMobileView(): self + { + ++$this->mobileViews; + + return $this; + } + public function getDate(): ?\DateTimeInterface { return $this->date; diff --git a/core/EventListener/AnalyticListener.php b/core/EventListener/AnalyticListener.php index fdec2f5..5fe1069 100644 --- a/core/EventListener/AnalyticListener.php +++ b/core/EventListener/AnalyticListener.php @@ -2,18 +2,19 @@ namespace App\Core\EventListener; -use App\Core\Manager\EntityManager; -use App\Core\Repository\Site\NodeRepositoryQuery; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use App\Core\Repository\Site\NodeRepository; -use App\Core\Repository\Analytic\ViewRepositoryQuery; -use App\Core\Factory\Analytic\ViewFactory; -use App\Core\Entity\Site\Node; -use Symfony\Component\HttpFoundation\Request; -use App\Core\Repository\Analytic\RefererRepositoryQuery; -use App\Core\Factory\Analytic\RefererFactory; use App\Core\Entity\EntityInterface; -use Jaybizzle\CrawlerDetect\CrawlerDetect; +use App\Core\Entity\Site\Node; +use App\Core\Factory\Analytic\RefererFactory; +use App\Core\Factory\Analytic\ViewFactory; +use App\Core\Manager\EntityManager; +use App\Core\Repository\Analytic\RefererRepositoryQuery; +use App\Core\Repository\Analytic\ViewRepositoryQuery; +use App\Core\Repository\Site\NodeRepository; +use DeviceDetector\Cache\PSR6Bridge; +use DeviceDetector\DeviceDetector; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; /** * class AnalyticListener. @@ -28,7 +29,7 @@ class AnalyticListener protected RefererRepositoryQuery $refererRepositoryQuery; protected RefererFactory $refererFactory; protected EntityManager $manager; - protected CrawlerDetect $crawlerDetect; + protected DeviceDetector $deviceDetector; protected Request $request; protected Node $node; @@ -46,7 +47,7 @@ class AnalyticListener $this->refererRepositoryQuery = $refererRepositoryQuery; $this->refererFactory = $refererFactory; $this->manager = $manager; - $this->crawlerDetect = new CrawlerDetect(); + $this->createDeviceDetector(); } public function onKernelRequest(RequestEvent $event) @@ -57,7 +58,10 @@ class AnalyticListener return; } - if ($this->crawlerDetect->isCrawler($request->headers->get('user-agent'))) { + $this->deviceDetector->setUserAgent($request->headers->get('user-agent')); + $this->deviceDetector->parse(); + + if ($this->deviceDetector->isBot()) { return; } @@ -77,6 +81,14 @@ class AnalyticListener $this->createReferer(); } + protected function createDeviceDetector() + { + $cache = new ApcuAdapter(); + + $this->deviceDetector = new DeviceDetector(); + $this->deviceDetector->setCache(new PSR6Bridge($cache)); + } + protected function createView() { $entity = $this->viewRepositoryQuery->create() @@ -90,6 +102,13 @@ class AnalyticListener } $entity->addView(); + + if ($this->deviceDetector->isDesktop()) { + $entity->addDesktopView(); + } elseif ($this->deviceDetector->isMobile()) { + $entity->addMobileView(); + } + $this->save($entity); } diff --git a/core/Resources/views/analytic/stats.html.twig b/core/Resources/views/analytic/stats.html.twig index 8190fa1..1d7e61d 100644 --- a/core/Resources/views/analytic/stats.html.twig +++ b/core/Resources/views/analytic/stats.html.twig @@ -47,18 +47,22 @@ {{ 'Path'|trans }} - {{ 'Views'|trans }} + {{ 'Views'|trans }} + + {% for path, views in pathViews %} {{ path }} - {{ views }} + {{ views.views }} + {{ views.desktopViews }} + {{ views.mobileViews }} {% else %} - +
diff --git a/symfony.lock b/symfony.lock index fb60411..33f0e16 100644 --- a/symfony.lock +++ b/symfony.lock @@ -160,9 +160,15 @@ "config/routes/liip_imagine.yaml" ] }, + "matomo/device-detector": { + "version": "5.0.4" + }, "monolog/monolog": { "version": "2.2.0" }, + "mustangostang/spyc": { + "version": "0.6.3" + }, "myclabs/php-enum": { "version": "1.8.0" }, From 24cd4069824f7439b012bda84d0e509c50e91dcb Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 25 Feb 2022 15:39:57 +0100 Subject: [PATCH 075/237] update changelog and upgrade doc --- CHANGELOG.md | 10 +++++++++- UPGRADE.md | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe294f..b8b845c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,22 @@ ## [Unreleased] ### Added -* update dependencies ### Fixed ### Changed +## 1.5.0 +### Added +* add desktop views and mobile views + +### Changed +* upgrade dependencies +* replace jaybizzle/crawler-detect with matomo/device-detector + ## 1.4.1 ### Added * handle app urls in twig routing filters ### Fixed + * fix views in analytics modal * replace empty path with "/" in analytics ### Changed diff --git a/UPGRADE.md b/UPGRADE.md index 6c79947..a4b457d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,6 @@ ## [Unreleased] +## Upgrade to v1.5.0 ### Commands ``` @@ -9,7 +10,6 @@ make doctrine-migration ``` ## Upgrade to v1.4.0 - ### Commands ``` From 263219850ec4fdf6401745e93c86f6427170137f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Fri, 25 Feb 2022 17:42:18 +0100 Subject: [PATCH 076/237] fix form namespace prefix in the crud controller maker --- core/Maker/MakeCrudController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Maker/MakeCrudController.php b/core/Maker/MakeCrudController.php index a90e7c1..d8e3f71 100644 --- a/core/Maker/MakeCrudController.php +++ b/core/Maker/MakeCrudController.php @@ -79,7 +79,7 @@ class MakeCrudController extends AbstractMaker $formDetails = $generator->createClassNameDetails( $input->getArgument('form-class'), - 'Type\\', + 'Form\\', '' ); From 8e3642f878bf6daee1e6019a4b3322f12d3f65e7 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 19:43:11 +0100 Subject: [PATCH 077/237] fix date field when the value is empty --- core/Resources/views/admin/crud/field/date.html.twig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/Resources/views/admin/crud/field/date.html.twig b/core/Resources/views/admin/crud/field/date.html.twig index 98c34c0..c1daaa0 100644 --- a/core/Resources/views/admin/crud/field/date.html.twig +++ b/core/Resources/views/admin/crud/field/date.html.twig @@ -1,4 +1,6 @@ - - - {{ value|date(options.format) }} - +{% if value %} + + + {{ value|date(options.format) }} + +{% endif %} From 46ecb3a468b8778da5deaadaa5ab760ce97e3862 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 19:43:56 +0100 Subject: [PATCH 078/237] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b845c..13b62ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added ### Fixed +* fix form namespace prefix in the crud controller maker +* fix date field when the value is empty ### Changed ## 1.5.0 From 9bc2ee26f94e9d8f47fdff2ecef7ece89ae2e8d1 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 19:55:44 +0100 Subject: [PATCH 079/237] add block in field templates to allow override --- .../views/admin/crud/field/button.html.twig | 8 +++++++- .../views/admin/crud/field/date.html.twig | 14 ++++++++------ .../views/admin/crud/field/image.html.twig | 4 +++- .../views/admin/crud/field/text.html.twig | 9 +++++++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/core/Resources/views/admin/crud/field/button.html.twig b/core/Resources/views/admin/crud/field/button.html.twig index 969ccf7..7f3ad4d 100644 --- a/core/Resources/views/admin/crud/field/button.html.twig +++ b/core/Resources/views/admin/crud/field/button.html.twig @@ -1 +1,7 @@ -<{{ options.button_tag }} {% for k, v in options.button_attr %}{{ k }}="{{ v }}"{% endfor %}>{% if options.raw %}{{ value|raw }}{% else %}{{ value|trans }}{% endif %} +{% extends '@Core/admin/crud/field/text.html.twig' %} + +{%- block value -%} + <{{- options.button_tag }} {% for k, v in options.button_attr %}{{ k }}="{{ v }}" {% endfor %}> + {{- parent() -}} + +{%- endblock -%} diff --git a/core/Resources/views/admin/crud/field/date.html.twig b/core/Resources/views/admin/crud/field/date.html.twig index c1daaa0..f336616 100644 --- a/core/Resources/views/admin/crud/field/date.html.twig +++ b/core/Resources/views/admin/crud/field/date.html.twig @@ -1,6 +1,8 @@ -{% if value %} - - - {{ value|date(options.format) }} - -{% endif %} +{%- block value -%} + {%- if value -%} + + + {{- value|date(options.format) -}} + + {%- endif -%} +{%- endblock -%} diff --git a/core/Resources/views/admin/crud/field/image.html.twig b/core/Resources/views/admin/crud/field/image.html.twig index 1efa29b..82b8e38 100644 --- a/core/Resources/views/admin/crud/field/image.html.twig +++ b/core/Resources/views/admin/crud/field/image.html.twig @@ -1 +1,3 @@ - +{%- block value -%} + +{%- endblock -%} diff --git a/core/Resources/views/admin/crud/field/text.html.twig b/core/Resources/views/admin/crud/field/text.html.twig index e9cf8ac..93377e2 100644 --- a/core/Resources/views/admin/crud/field/text.html.twig +++ b/core/Resources/views/admin/crud/field/text.html.twig @@ -1,2 +1,7 @@ -{% if options.raw %}{{ value|raw }}{% else %}{{ value|trans }}{% endif %} - +{%- block value -%} + {%- if options.raw -%} + {{- value|raw -}} + {%- else -%} + {{- value|trans -}} + {%- endif -%} +{%- endblock -%} From 03e401f9ce6851ca3754a8cf146281b5bf66f095 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 19:56:33 +0100 Subject: [PATCH 080/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b62ff..d59fc7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added +* add block in field templates to allow override ### Fixed * fix form namespace prefix in the crud controller maker * fix date field when the value is empty From 661840d87caa651a2f1dd306784b271cc5564bac Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 20:09:46 +0100 Subject: [PATCH 081/237] fix crud batch column width --- assets/css/admin.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 40bfaf3..c804467 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -490,9 +490,8 @@ fieldset.form-group { } } -th.crud-batch-column { - width: 20px !important; - max-width: 20px; +.table .crud-batch-column { + width: 1%; } form { From 0af04c913128e20c1da402b5ac5e9624ea26dfb7 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 20:32:13 +0100 Subject: [PATCH 082/237] merge route params in crud admin redirects --- core/Controller/Admin/Crud/CrudController.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/Controller/Admin/Crud/CrudController.php b/core/Controller/Admin/Crud/CrudController.php index c6b223e..57f6688 100644 --- a/core/Controller/Admin/Crud/CrudController.php +++ b/core/Controller/Admin/Crud/CrudController.php @@ -68,9 +68,10 @@ abstract class CrudController extends AdminController $entityManager->create($entity); $this->addFlash('success', 'The data has been saved.'); - return $this->redirectToRoute($configuration->getPageRoute('edit'), [ - 'entity' => $entity->getId(), - ]); + return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge( + ['entity' => $entity->getId()], + $configuration->getPageRouteParams('edit') + )); } $this->addFlash('warning', 'The form is not valid.'); } @@ -111,9 +112,10 @@ abstract class CrudController extends AdminController $entityManager->update($entity); $this->addFlash('success', 'The data has been saved.'); - return $this->redirectToRoute($configuration->getPageRoute('edit'), [ - 'entity' => $entity->getId(), - ]); + return $this->redirectToRoute($configuration->getPageRoute('edit'), array_merge( + ['entity' => $entity->getId()], + $configuration->getPageRouteParams('edit') + )); } $this->addFlash('warning', 'The form is not valid.'); } From 90499d8dc9252572f33dc8e7fd102493674baf0a Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 20:54:46 +0100 Subject: [PATCH 083/237] fix sidebar icon width --- assets/css/admin.scss | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/assets/css/admin.scss b/assets/css/admin.scss index c804467..9b9a739 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -163,8 +163,13 @@ tr.table-primary-light { .fa { font-size: 1.2rem; margin-right: 5px; - min-width: 30px; color: #fff; + width: 35px; + display: inline-block; + } + + .nav-item-label { + display: inline-block; } &.active { @@ -245,10 +250,10 @@ tr.table-primary-light { .nav-link { padding-left: 10px; - } - .nav-item-label { - display: none; + .nav-item-label { + display: none; + } } .sidebar-heading { From 10a1168009b11f4995711cf342ec0367732788ed Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 20:58:36 +0100 Subject: [PATCH 084/237] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59fc7f..90aa85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ### Added * add block in field templates to allow override +* merge route params in crud admin redirects ### Fixed * fix form namespace prefix in the crud controller maker * fix date field when the value is empty +* fix crud batch column width +* fix sidebar icon width ### Changed ## 1.5.0 From 83d75d2415f53b5f43c68556f920f7924b2268e0 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 23:13:18 +0100 Subject: [PATCH 085/237] fix cache clear task --- core/Cache/SymfonyCacheManager.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/core/Cache/SymfonyCacheManager.php b/core/Cache/SymfonyCacheManager.php index 5504be8..a368cd0 100644 --- a/core/Cache/SymfonyCacheManager.php +++ b/core/Cache/SymfonyCacheManager.php @@ -63,15 +63,6 @@ class SymfonyCacheManager $output = new BufferedOutput(); } - $input = new ArrayInput([ - 'command' => 'cache:clear', - '-e' => $this->kernel->getEnvironment(), - '--no-warmup' => null, - '--ansi' => null, - ]); - - $application->run($input, $output); - $input = new ArrayInput([ 'command' => 'cache:warmup', '-e' => $this->kernel->getEnvironment(), From ded0279514341b76b25502b6cd3622190e38c82d Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 26 Feb 2022 23:22:38 +0100 Subject: [PATCH 086/237] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aa85a..f95ce3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * fix date field when the value is empty * fix crud batch column width * fix sidebar icon width +* fix cache clear task ### Changed ## 1.5.0 From e81f8be151e962eef7392022468cc5420618530a Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 09:35:54 +0100 Subject: [PATCH 087/237] remove password generation from the user factory --- core/Factory/UserFactory.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/core/Factory/UserFactory.php b/core/Factory/UserFactory.php index 9d4d4bc..0218062 100644 --- a/core/Factory/UserFactory.php +++ b/core/Factory/UserFactory.php @@ -16,9 +16,8 @@ class UserFactory implements FactoryInterface protected TokenGeneratorInterface $tokenGenerator; protected UserPasswordEncoderInterface $encoder; - public function __construct(TokenGeneratorInterface $tokenGenerator, UserPasswordEncoderInterface $encoder) + public function __construct(UserPasswordEncoderInterface $encoder) { - $this->tokenGenerator = $tokenGenerator; $this->encoder = $encoder; } @@ -26,14 +25,13 @@ class UserFactory implements FactoryInterface { $entity = new User(); - if (!empty($email)) { + if (null !== $email) { $entity->setEmail($email); } - $entity->setPassword($this->encoder->encodePassword( - $entity, - !empty($password) ? $password : $this->tokenGenerator->generateToken() - )); + if (null !== $email) { + $entity->setPassword($this->encoder->encodePassword($entity, $password)); + } return $entity; } From 85e4bdf1c256f4e2f192f7facfdcb7fa3df212b4 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 09:37:22 +0100 Subject: [PATCH 088/237] improve murph:user:create command (add sections and the generated password is displayed) --- core/Command/UserCreateCommand.php | 43 +++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/core/Command/UserCreateCommand.php b/core/Command/UserCreateCommand.php index 2248210..598bfaf 100644 --- a/core/Command/UserCreateCommand.php +++ b/core/Command/UserCreateCommand.php @@ -7,10 +7,12 @@ use App\Core\Manager\EntityManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; class UserCreateCommand extends Command { @@ -18,11 +20,16 @@ class UserCreateCommand extends Command protected static $defaultDescription = 'Creates a user'; protected UserFactory $userFactory; protected EntityManager $entityManager; + protected TokenGeneratorInterface $tokenGenerator; - public function __construct(UserFactory $userFactory, EntityManager $entityManager) - { + public function __construct( + UserFactory $userFactory, + EntityManager $entityManager, + TokenGeneratorInterface $tokenGenerator + ) { $this->userFactory = $userFactory; $this->entityManager = $entityManager; + $this->tokenGenerator = $tokenGenerator; parent::__construct(); } @@ -32,6 +39,8 @@ class UserCreateCommand extends Command $this ->setDescription(self::$defaultDescription) ->addArgument('email', InputArgument::OPTIONAL, 'E-mail') + ->addOption('is-admin', null, InputOption::VALUE_NONE, 'Add the admin role') + ->addOption('is-writer', null, InputOption::VALUE_NONE, 'Add the write role') ; } @@ -43,9 +52,7 @@ class UserCreateCommand extends Command $emailQuestion = new Question('E-mail: '); $emailQuestion->setValidator(function ($value) { if (empty($value)) { - throw new \RuntimeException( - 'The email must not be empty.' - ); + throw new \RuntimeException('The email must not be empty.'); } return $value; @@ -53,8 +60,17 @@ class UserCreateCommand extends Command $passwordQuestion = new Question('Password (leave empty to generate a random password): '); $passwordQuestion->setHidden(true); - $isAdminQuestion = new ConfirmationQuestion('Is admin? [y/n] ', false); - $isWriterQuestion = new ConfirmationQuestion('Is writer? [y/n] ', false); + + $isAdminDefault = $input->getOption('is-admin'); + $isWriterDefault = $input->getOption('is-writer'); + + $isAdminQuestionLabel = sprintf('Administrator [%s] ', $isAdminDefault ? 'Y/n' : 'y/N'); + $isWriterQuestionLabel = sprintf('Writer [%s] ', $isWriterDefault ? 'Y/n' : 'y/N'); + + $isAdminQuestion = new ConfirmationQuestion($isAdminQuestionLabel, $isAdminDefault); + $isWriterQuestion = new ConfirmationQuestion($isWriterQuestionLabel, $isWriterDefault); + + $io->section('Authentication'); $email = $input->getArgument('email'); @@ -63,6 +79,18 @@ class UserCreateCommand extends Command } $password = $helper->ask($input, $output, $passwordQuestion); + + $showPassword = empty($password); + + if ($showPassword) { + $password = mb_substr($this->tokenGenerator->generateToken(), 0, 18); + $io->info(sprintf('Password: %s', $password)); + } else { + $io->newLine(); + } + + $io->section('Roles'); + $isAdmin = $helper->ask($input, $output, $isAdminQuestion); $isWriter = $helper->ask($input, $output, $isWriterQuestion); @@ -72,6 +100,7 @@ class UserCreateCommand extends Command $this->entityManager->create($user); + $io->newLine(); $io->success('User created!'); return Command::SUCCESS; From 6134b30a998fa30672525254f6048ae119853c51 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 09:44:18 +0100 Subject: [PATCH 089/237] update changelog --- CHANGELOG.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f95ce3c..36fc1e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,26 @@ ## [Unreleased] +### Added +### Fixed +### Changed + +## [1.6.0] - 2022-02-28 ### Added * add block in field templates to allow override * merge route params in crud admin redirects +* improve murph:user:create command + ### Fixed * fix form namespace prefix in the crud controller maker * fix date field when the value is empty * fix crud batch column width * fix sidebar icon width * fix cache clear task -### Changed -## 1.5.0 +### Changed +* remove password generation from the user factory + +## [1.5.0] - 2022-02-25 ### Added * add desktop views and mobile views @@ -19,21 +28,21 @@ * upgrade dependencies * replace jaybizzle/crawler-detect with matomo/device-detector -## 1.4.1 +## [1.4.1] - 2022-02-23 ### Added * handle app urls in twig routing filters -### Fixed +### Fixed * fix views in analytics modal * replace empty path with "/" in analytics ### Changed * update default templates -## 1.4.0 +## [1.4.0] - 2022-02-21 ### Added * add basic analytics -## 1.3.0 +## [1.3.0] - 2022-02-19 ### Added * add support of regexp with substitution in redirect * url tags can be used as redirect location @@ -43,7 +52,7 @@ * fix filemanager sorting * fix batch action setter -## 1.2.0 +## [1.2.0] - 2022-02-14 ### Added * add sort in file manager * add redirect manager @@ -51,7 +60,7 @@ ### Changed * replace node-sass with sass -## 1.1.0 +## [1.1.0] - 2022-02-29 ### Added * add directory upload in file manager @@ -61,9 +70,9 @@ ### Changed * symfony/swiftmailer-bundle is replaced by symfony/mailer -## 1.0.1 +## [1.0.1] - 2022-02-25 ### Fixed * fix Makefile environment vars (renaming) * fix composer minimum stability -## 1.0.0 +## [1.0.0] - 2022-01-23 From 551e4beaa38821b6f91d103986308525f662011f Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 11:33:28 +0100 Subject: [PATCH 090/237] add sidebar scroll on tablet --- assets/css/admin.scss | 16 +++++++++++----- .../views/site/tree_admin/navigation.html.twig | 6 ++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 9b9a739..ac25184 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -229,21 +229,27 @@ tr.table-primary-light { } } +@media screen and (max-width: 1080px) { + .sidebar-sticky { + overflow-y: auto !important; + } +} + @media screen and (max-width: 770px) { .body { margin-left: 50px; width: calc(100vw - 50px); } + .sidebar-sticky { + width: 50px; + max-width: 100% !important; + } + .sidebar { width: 50px; max-width: 100% !important; - .sidebar-sticky { - width: 50px; - max-width: 100% !important; - } - .nav { padding-left: 0; } diff --git a/core/Resources/views/site/tree_admin/navigation.html.twig b/core/Resources/views/site/tree_admin/navigation.html.twig index 31efad1..5b16b3c 100644 --- a/core/Resources/views/site/tree_admin/navigation.html.twig +++ b/core/Resources/views/site/tree_admin/navigation.html.twig @@ -125,7 +125,7 @@
-
+ -
+
From b9d929fddfc06694cb9cfd99066c17109c8abac2 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 13:56:09 +0100 Subject: [PATCH 091/237] upgrade dependencies --- CHANGELOG.md | 1 + UPGRADE.md | 6 ++++++ assets/js/admin/modules/sortable.js | 1 + package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36fc1e6..37f6574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added ### Fixed ### Changed +* upgrade dependencies ## [1.6.0] - 2022-02-28 ### Added diff --git a/UPGRADE.md b/UPGRADE.md index a4b457d..af57b61 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,11 @@ ## [Unreleased] +### Commands + +``` +yarn add sortablejs@^1.14.0 +``` + ## Upgrade to v1.5.0 ### Commands diff --git a/assets/js/admin/modules/sortable.js b/assets/js/admin/modules/sortable.js index 7f2b7bf..b096176 100644 --- a/assets/js/admin/modules/sortable.js +++ b/assets/js/admin/modules/sortable.js @@ -11,6 +11,7 @@ module.exports = () => { sort: true, animation: 150, fallbackTolerance: 3, + delayOnTouchOnly: true, onEnd: (e) => { if (!route) { return diff --git a/package.json b/package.json index 25ed802..17c4ddc 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "jquery": "^3.6.0", "popper.js": "^1.16.0", "qrcodejs": "^1.0.0", - "sortablejs": "^1.13.0", + "sortablejs": "^1.14.0", "tinymce": "^5.7.1", "vanillajs-datepicker": "^1.1.2", "vue": "^2.6.14", diff --git a/yarn.lock b/yarn.lock index 6faa9fb..759247a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5563,10 +5563,10 @@ sockjs@0.3.21: uuid "^3.4.0" websocket-driver "^0.7.4" -sortablejs@^1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.13.0.tgz#3ab2473f8c69ca63569e80b1cd1b5669b51269e9" - integrity sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg== +sortablejs@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8" + integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w== source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" From 25940fc188d1ea8285e8ca38ae04455d9ac05ebf Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Mon, 28 Feb 2022 14:18:45 +0100 Subject: [PATCH 092/237] move assets to the core directory of Murph --- assets/css/_admin_extend.scss | 1 - assets/css/_admin_vars.scss | 0 assets/css/admin.scss | 536 +----------------- assets/images/blank.png | Bin 163 -> 0 bytes assets/images/no-image.png | Bin 1536 -> 0 bytes assets/js/admin.js | 2 +- core/Resources/assets/css/admin.scss | 531 +++++++++++++++++ .../Resources/assets/js}/admin.js | 4 +- .../js}/components/file-manager/FileIcon.vue | 4 +- .../components/file-manager/FileManager.vue | 0 .../js}/components/file-manager/Files.vue | 4 +- .../Resources/assets/js}/modules/analytics.js | 0 .../Resources/assets/js}/modules/batch.js | 0 .../assets/js}/modules/checkbox-checker.js | 0 .../Resources/assets/js}/modules/choices.js | 0 .../assets/js}/modules/datepicker.js | 0 .../Resources/assets/js}/modules/dbclick.js | 0 .../assets/js}/modules/document-selector.js | 0 .../Resources/assets/js}/modules/editor.js | 0 .../assets/js}/modules/file-manager.js | 0 .../assets/js}/modules/file-picker.js | 0 .../Resources/assets/js}/modules/form-ajax.js | 0 .../assets/js}/modules/form-collection.js | 0 .../assets/js}/modules/form-confirm.js | 0 .../assets/js}/modules/form-error.js | 0 .../Resources/assets/js}/modules/form-file.js | 0 .../Resources/assets/js}/modules/modal.js | 0 .../Resources/assets/js}/modules/panel.js | 0 .../Resources/assets/js}/modules/password.js | 0 .../assets/js}/modules/push-state.js | 0 .../assets/js}/modules/rest-choices.js | 0 .../Resources/assets/js}/modules/sortable.js | 1 - .../assets/js}/modules/table-fixed.js | 0 .../assets/js}/modules/table-selectable.js | 0 .../Resources/assets/js}/modules/toast.js | 0 .../Resources/assets/js}/modules/tooltip.js | 0 36 files changed, 539 insertions(+), 544 deletions(-) delete mode 100644 assets/css/_admin_extend.scss delete mode 100644 assets/css/_admin_vars.scss delete mode 100644 assets/images/blank.png delete mode 100644 assets/images/no-image.png create mode 100644 core/Resources/assets/css/admin.scss rename {assets/js/admin => core/Resources/assets/js}/admin.js (89%) rename {assets/js/admin => core/Resources/assets/js}/components/file-manager/FileIcon.vue (91%) rename {assets/js/admin => core/Resources/assets/js}/components/file-manager/FileManager.vue (100%) rename {assets/js/admin => core/Resources/assets/js}/components/file-manager/Files.vue (98%) rename {assets/js/admin => core/Resources/assets/js}/modules/analytics.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/batch.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/checkbox-checker.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/choices.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/datepicker.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/dbclick.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/document-selector.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/editor.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/file-manager.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/file-picker.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/form-ajax.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/form-collection.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/form-confirm.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/form-error.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/form-file.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/modal.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/panel.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/password.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/push-state.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/rest-choices.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/sortable.js (96%) rename {assets/js/admin => core/Resources/assets/js}/modules/table-fixed.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/table-selectable.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/toast.js (100%) rename {assets/js/admin => core/Resources/assets/js}/modules/tooltip.js (100%) diff --git a/assets/css/_admin_extend.scss b/assets/css/_admin_extend.scss deleted file mode 100644 index 8b13789..0000000 --- a/assets/css/_admin_extend.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/css/_admin_vars.scss b/assets/css/_admin_vars.scss deleted file mode 100644 index e69de29..0000000 diff --git a/assets/css/admin.scss b/assets/css/admin.scss index ac25184..b72440a 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -1,535 +1 @@ -@import "./_admin_vars.scss"; - -$theme-colors: ( - "primary": #1ab5dc, - "primary-light": lighten(#3183aa, 40%), - "dark-blue": #1e2430, -) !default; - -$grid-gutter-width: 0px !default; -$pagination-color: #343a40 !default; -$pagination-bg: #ffffff !default; -$pagination-active-color: #ffffff !default; -$pagination-active-bg: #343a40 !default; - -@import "~choices.js/src/styles/choices.scss"; -@import "~bootstrap/scss/bootstrap.scss"; -@import "~@fortawesome/fontawesome-free/css/all.css"; -@import "~flag-icon-css/sass/flag-icon.scss"; - -@for $i from 1 through 100 { - .miw-#{$i*5} { - min-width: $i * 5px; - } -} - -.flag-icon-en { - background-image: url(~flag-icon-css/flags/4x3/gb.svg); -} - -body { - overflow-x: hidden; -} - -#logo { - width: 30px; -} - -.choices__list--dropdown { - display: none; -} - -.choices__list--dropdown.is-active { - display: block; -} - -.dropdown-toggle-hide-after { - &::after { - display: none; - } -} - -.login { - &-container { - margin-top: 5%; - margin-bottom: 5%; - } - - &-form { - padding: 5%; - } - - &-image { - width: 100%; - max-width: 80%; - } -} - -.sidebar { - position: fixed; - top: 0; - bottom: 0; - left: 0; - z-index: 100; - padding: 71px 0 0; - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); -} - -.sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 71px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: hidden; - - &:hover { - overflow-y: auto; - } -} - -@supports ((position: -webkit-sticky) or (position: sticky)) { - .sidebar-sticky { - position: -webkit-sticky; - position: sticky; - } -} - -.actions-container { - padding-right: 25px; -} - -.table .thead-light { - a, th { - color: map-get($theme-colors, 'dark-blue'); - } -} - -tr.table-primary-light { - background-color: #ecf5fa; -} - -.td-nowrap { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.table tr { - td { - transition: border 500ms ease-out; - border-bottom: 1px solid #dee2e6; - } - - &:hover { - td { - border-bottom: 1px solid #a8aaac; - } - } -} - -.bg-dark-blue { - background: map-get($theme-colors, 'dark-blue'); - color: #fff; - - .nav-item-label { - color: #fff; - } -} - -.nav-pills { - .nav-item { - margin-right: 3px; - } - - .nav-link:not(.active) { - color: #333; - background: #eee; - } -} - - -.sidebar { - width: 260px; - display: inline-block; - - .nav-link { - font-weight: 500; - color: #333; - border-left: 4px solid map-get($theme-colors, 'dark-blue'); - padding-top: 14px; - padding-bottom: 14px; - - .fa { - font-size: 1.2rem; - margin-right: 5px; - color: #fff; - width: 35px; - display: inline-block; - } - - .nav-item-label { - display: inline-block; - } - - &.active { - font-weight: bold; - border-left: 4px solid map-get($theme-colors, 'primary'); - background: map-get($theme-colors, 'dark-blue'); - } - } - - &-heading { - font-size: .75rem; - text-transform: uppercase; - display: flex; - } -} - -*[data-selectable-selector] { - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; -} - -*[data-selectable-selector] { - &:hover { - cursor: pointer; - } -} - -*[data-sortable-item] { - &:hover { - cursor: pointer; - } - - &.sortable-chosen { - background: map-get($theme-colors, 'primary-light'); - } -} - -.footer { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - z-index: 1000; - height: 35px; - background: #f8f9fa; -} - -.body { - padding-top: 60px; - width: calc(100% - 260px); - margin-left: 260px; - display: inline-block; - - .nav { - padding-left: 10px; - } -} - -@media screen and (max-width: 1080px) { - .sidebar-sticky { - overflow-y: auto !important; - } -} - -@media screen and (max-width: 770px) { - .body { - margin-left: 50px; - width: calc(100vw - 50px); - } - - .sidebar-sticky { - width: 50px; - max-width: 100% !important; - } - - .sidebar { - width: 50px; - max-width: 100% !important; - - .nav { - padding-left: 0; - } - - .nav-link { - padding-left: 10px; - - .nav-item-label { - display: none; - } - } - - .sidebar-heading { - display: none; - } - } -} - -th { - &.sorted { - &::before { - content: '\f0dc'; - font-family: 'FontAwesome'; - color: #aaa; - margin-right: 3px; - } - } -} - -.table-responsive { - max-width: 100%; - overflow-y: hidden; -} - -.toast-container { - display: flex; - position: relative; - z-index: 4000; - - .toast-wrapper { - position: fixed; - top: 20px; - right: 20px; - z-index: 1060; - width: 300px; - } -} - -.bg-tiles { - background-color: #c1c1c1; - background-image: linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%), linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%); - background-size: 20px 20px; - background-position: 0 0, 10px 10px; -} - -.tab-form { - padding: 15px; -} - -.icon-margin { - margin-right: 4px; -} - -.file-icon { - font-size: 2em; -} - -.d-ib { - display: inline-block; -} - -.list-checkbox { - vertical-align: middle; - margin-right: 10px; - margin-top: -2px; -} - -.password-strenth { - padding: 0 0 5px 0; - margin-top: -4px; - - .col-sm { - height: 8px; - border: 2px solid #fff; - } - - &-info { - font-size: 13px; - height: 22px; - } -} - -.notification-bell:not([disabled]) { - [data-counter]:after { - display: block; - color: #fff; - background: red; - width: 9px; - height: 9px; - position: absolute; - content: ' '; - top: 4px; - right: 10px; - border-radius: 4px; - } -} - -.form-error-icon { - margin-right: 4px; -} - -.custom-file-label::after { - content: "Parcourir"; -} - -#lease_template_html { - height: calc(100vh - 270px); -} - -.panel { - &-toggler { - &:hover { - cursor: pointer; - } - } - - &-content { - display: block; - - &:not(.active) { - display: none; - } - } -} - -*[data-collection-delete-container] { - cursor: pointer; -} - -*[data-collection-add] { - cursor: pointer; -} - -.login-image { - width: 50%; -} - -.tree { - position: relative; - background: white; - color: #212529; - - span { - font-style: italic; - letter-spacing: .4px; - color: #a8a8a8; - } - - .fa-folder-open, .fa-folder { - color: #007bff; - } - - .fa-html5 { - color: #f21f10; - } - - ul { - padding-left: 5px; - list-style: none; - margin: 0; - padding-bottom: 0; - - li { - position: relative; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 15px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - - &:before { - position: absolute; - top: 15px; - left: 0; - width: 10px; - height: 1px; - margin: auto; - content: ''; - background-color: #666; - } - - &:after { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 1px; - height: 100%; - content: ''; - background-color: #666; - } - - &:last-child:after { - height: 15px; - } - } - - a { - cursor: pointer; - - &:hover { - text-decoration: none; - } - } - } -} - -fieldset.form-group { - margin-bottom: 0; -} - -.crud-header { - &-title { - font-size: 2em; - } - - &-actions { - text-align: right; - margin-bottom: 10px; - } - - @media screen and (min-width: 770px) { - &-title { - float: left; - font-size: 2em; - } - - &-actions { - float: right; - } - - } - - &::after { - display: block; - content: ""; - clear: both; - } -} - -.table .crud-batch-column { - width: 1%; -} - -form { - .loader { - display: none; - } - - &.is-loading .loader { - display: inline-block; - } -} - -.modal { - z-index: 3000; -} - -.modal-dialog-large { - max-width: 80%; - margin-left: auto; - margin-right: auto; -} - -.output { - &-console { - background: #073642; - line-height: normal; - } -} - -@import "./_admin_extend.scss"; +@import "../../core/Resources/css/admin.scss"; diff --git a/assets/images/blank.png b/assets/images/blank.png deleted file mode 100644 index 5e21c8a9d64b6b89586c0cd1e2a0ced98ee35c98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-mSQK*5Dp-y;YjHK@;M7UB8wRq zIOIW?@rCpD)j&bX64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq=1SPJY5_^ sG8*3=WMl;LniT%8&+~8qF~Q)#Vn%f*2F8>=Lpu=P)78&qol`;+02@6fmjD0& diff --git a/assets/images/no-image.png b/assets/images/no-image.png deleted file mode 100644 index 7957221866d0ebf40bc0649eb2b32f0eb5fe8c2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1536 zcmai!dr(qo7{<|}5xgVWz}wGNm{94s7E~M(I7KZED$v+Q&$u-yR1`z zrL}2}ipbE>COc}E4Kgb#-C86x*UN@xtXh_9<)uCCzdO78&b;q$p6{FY`TqKzYpzV!ljoo(%FE;uPW__*{sz3UXqY&;0k*?8L_!_VbR$W$foG?B)!;zi@l3Demgx znuuIO2D`g4#b=>PH6U*4nA}jJZ=HtZ6OX4X7J5t1sIm7LzudAewC1dN7~pk}C}y&E zL`L2@?fT_i?TMecFT9?x0Ze3Meeam*zzszWVAZo33wMv~E4}~WVl~{=cq z+lM4>@8sE6wk26<9DCiS-pOjq3R%XjxZ&ndgqzG%X)KQ~uPohoc;lteYpJ(x-UOG| zoFBY$Bqv95xbgM4{3!$pg+i@&Wg;glJG)u^aHNIRA6>m5Ecbm$ht?dUR`PO(=ki+Y#|fTI<|QS+r_9j$P#vyBmSP`bH;ZctOX^a0Pm2@$if1g&&j zWG=wF%dqCYM+9?}*3yafAKZxW7cqbWlxQ|94FR`;7Xp5xLwk>kLccN|&P;A90oF6r;bR&X}^O+T& zX$wKv4<(Zh!i!PvJP7j+L#&Aw2LG-Y$$xXuq_xgL^<+Zx;5|dJ-S8&%yB{%N^j8GukL+X+zfe$*e%F^b^9OxspkCwns%cLsX#7ItJa3 zeFy(MuY@zR9iIJQhrWA>=i)U{ABe+sULN%8ro?3S?oPouw^vHn__W>!-oXscDV$Lf zsA~xDUJC6Z+v?{S&Wp%+!RR?{SBySfwFYcIdHWtLTq!S-PW<6(=$dklP%Qwtn~!M8 zTmr22*eJ84nsnn74`NZ6$?{jdFs;{@z?`&1=57AekY7!gP;NgQv(9nH)8MQUh^y zBp`n6K#>DCNqJwaBa_wPVW-UbbP8BiNzr4YNO1m}#I=3Vt_n^MGXtvAI;p0rP71t1 z7MZI;xVco|c`(5^0Ou#i)%Hh|6r2rF!H9Dn74WH&3J4%8AqJ1UVFr@sKD*k zlX`mvXIX?Afp|DWLu;XaQ6~h%8gT~VhCD$_PwCY*5;xX(E$mXLhrWtrf}p9HpPDsn z<8FN2{+NpBrY5AT(L45-r~1iW;&gPyxWGZM? ztO+NoETxV6O({M=WUE diff --git a/core/Resources/assets/js/components/file-manager/FileManager.vue b/core/Resources/assets/js/components/file-manager/FileManager.vue deleted file mode 100644 index eab7f4a..0000000 --- a/core/Resources/assets/js/components/file-manager/FileManager.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/core/Resources/assets/js/components/file-manager/Files.vue b/core/Resources/assets/js/components/file-manager/Files.vue deleted file mode 100644 index 6e6af3d..0000000 --- a/core/Resources/assets/js/components/file-manager/Files.vue +++ /dev/null @@ -1,378 +0,0 @@ - - - - - diff --git a/core/Resources/assets/js/modules/analytics.js b/core/Resources/assets/js/modules/analytics.js deleted file mode 100644 index dcb6d1a..0000000 --- a/core/Resources/assets/js/modules/analytics.js +++ /dev/null @@ -1,53 +0,0 @@ -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/core/Resources/assets/js/modules/batch.js b/core/Resources/assets/js/modules/batch.js deleted file mode 100644 index 2d5c1f2..0000000 --- a/core/Resources/assets/js/modules/batch.js +++ /dev/null @@ -1,23 +0,0 @@ -const $ = require('jquery') - -module.exports = () => { - $('th.crud-batch-column input').change((e) => { - $('td.crud-batch-column input').prop('checked', $(e.target).is(':checked')) - }) - - const form = $('#form-batch') - - form.submit((e) => { - e.preventDefault() - - const route = form.attr('action') - const datas = form.serialize() - - form.addClass('is-loading') - - $.post(route, datas) - .always(() => { - document.location.reload() - }) - }) -} diff --git a/core/Resources/assets/js/modules/checkbox-checker.js b/core/Resources/assets/js/modules/checkbox-checker.js deleted file mode 100644 index c6804a5..0000000 --- a/core/Resources/assets/js/modules/checkbox-checker.js +++ /dev/null @@ -1,31 +0,0 @@ -const $ = require('jquery') - -module.exports = function () { - $('*[data-checkbox-ckecker]').click(function () { - const wrapperName = $(this).attr('data-checkbox-ckecker') - - if (!wrapperName) { - return - } - - const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]') - - $(checkboxes).each(function (i, v) { - $(v).prop('checked', true) - }) - }) - - $('*[data-checkbox-unckecker]').click(function () { - const wrapperName = $(this).attr('data-checkbox-unckecker') - - if (!wrapperName) { - return - } - - const checkboxes = $('*[data-checkbox-wrapper="' + wrapperName + '"] *[data-checkbox] input[type="checkbox"]') - - $(checkboxes).each(function (i, v) { - $(v).prop('checked', false) - }) - }) -} diff --git a/core/Resources/assets/js/modules/choices.js b/core/Resources/assets/js/modules/choices.js deleted file mode 100644 index d2da709..0000000 --- a/core/Resources/assets/js/modules/choices.js +++ /dev/null @@ -1,8 +0,0 @@ -const Choices = require('choices.js') -const $ = require('jquery') - -module.exports = function () { - $('*[data-jschoice]').each(function (key, item) { - return new Choices(item) - }) -} diff --git a/core/Resources/assets/js/modules/datepicker.js b/core/Resources/assets/js/modules/datepicker.js deleted file mode 100644 index 673ae32..0000000 --- a/core/Resources/assets/js/modules/datepicker.js +++ /dev/null @@ -1,30 +0,0 @@ -const Datepicker = require('vanillajs-datepicker') - -const isDateSupported = () => { - const input = document.createElement('input') - const value = 'a' - - input.setAttribute('type', 'date') - input.setAttribute('value', value) - - return input.value !== value -} - -const createDatePicker = (input) => { - return new Datepicker.Datepicker(input, { - format: 'yyyy-mm-dd' - }) -} - -module.exports = () => { - if (isDateSupported()) { - return - } - - const inputs = document.querySelectorAll('input[type="date"]') - const size = inputs.length - - for (let i = 0, c = size; i < c; i++) { - createDatePicker(inputs[i]) - } -} diff --git a/core/Resources/assets/js/modules/dbclick.js b/core/Resources/assets/js/modules/dbclick.js deleted file mode 100644 index 57713cf..0000000 --- a/core/Resources/assets/js/modules/dbclick.js +++ /dev/null @@ -1,7 +0,0 @@ -const $ = require('jquery') - -module.exports = function () { - $('*[data-dblclick]').dblclick(function (e) { - document.location.href = $(this).attr('data-dblclick') - }) -} diff --git a/core/Resources/assets/js/modules/document-selector.js b/core/Resources/assets/js/modules/document-selector.js deleted file mode 100644 index 604f77f..0000000 --- a/core/Resources/assets/js/modules/document-selector.js +++ /dev/null @@ -1,42 +0,0 @@ -const $ = require('jquery') - -const DocumentSelector = () => { - const forms = $('.document-selector-form') - - const handler = function () { - forms.each((fi, f) => { - const form = $(f) - const ids = form.find('.document-selector-ids') - const btn = form.find('.document-selector-button') - - ids.html('') - let hasSelection = false - - $('*[data-documents] *[data-selectable-row] input[data-selectable-checkbox]').each((i, c) => { - const checkbox = $(c) - - if (checkbox.is(':checked')) { - ids.append(checkbox[0].outerHTML) - hasSelection = true - } - }) - - if (hasSelection && btn.length) { - btn.removeAttr('disabled') - ids.find('input').prop('checked', true) - } else { - btn.attr('disabled', 'disabled') - } - }) - } - - $('*[data-documents] *[data-selectable-row]').click(function () { - window.setTimeout(handler, 100) - }) - - $('*[data-documents] *[data-selectable-row]').on('clicked', function () { - window.setTimeout(handler, 100) - }) -} - -module.exports = DocumentSelector diff --git a/core/Resources/assets/js/modules/editor.js b/core/Resources/assets/js/modules/editor.js deleted file mode 100644 index 7bfba77..0000000 --- a/core/Resources/assets/js/modules/editor.js +++ /dev/null @@ -1,626 +0,0 @@ -const $ = require('jquery') -const Vue = require('vue').default -const FileManager = require('../components/file-manager/FileManager').default - -const createModal = function () { - let container = $('#fm-modal') - const body = $('body') - - if (!container.length) { - container = $('