diff --git a/.env.test b/.env.test index 9e7162f..4502496 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st' SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots +DATABASE_URL="mysql://root:@127.0.0.1:3306/mediatekformation_test?serverVersion=8.0" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..d435bb9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +name: Generate Documentation + +on: + push: + branches: + - main + workflow_dispatch: +jobs: + phpdoc: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: phpdoc # Installe automatiquement phpDocumentor + + - name: Run phpDocumentor + run: phpdoc -d ./src -t ./docs/api + # -d : dossier source + # -t : dossier de destination pour la doc générée + + - name: Upload Documentation + uses: actions/upload-artifact@v4 + with: + name: php-doc-api + path: ./docs/api # Le dossier où phpDoc a généré l'HTML diff --git a/.gitignore b/.gitignore index 4daae38..dff5219 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ /public/assets/ /assets/vendor/ ###< symfony/asset-mapper ### + +/tests/ \ No newline at end of file diff --git a/composer.json b/composer.json index 2a284c5..fd93f22 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.1", "phpdocumentor/reflection-docblock": "^5.4", + "phpdocumentor/shim": "*", "phpstan/phpdoc-parser": "^1.29", "symfony/apache-pack": "^1.0", "symfony/asset": "6.4.*", @@ -49,6 +50,7 @@ "config": { "allow-plugins": { "php-http/discovery": true, + "phpdocumentor/shim": true, "symfony/flex": true, "symfony/runtime": true }, @@ -100,7 +102,7 @@ "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", "symfony/debug-bundle": "6.4.*", - "symfony/maker-bundle": "^1.0", + "symfony/maker-bundle": "^1.65", "symfony/phpunit-bridge": "^7.0", "symfony/stopwatch": "6.4.*", "symfony/web-profiler-bundle": "6.4.*" diff --git a/composer.lock b/composer.lock index 115a876..25f1aac 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "449a04e5b2ca2b8ce88b30d4b38fa5c0", + "content-hash": "5c2a0f302f462514c4808d0320c81cef", "packages": [ { "name": "composer/semver", @@ -1477,6 +1477,220 @@ ], "time": "2024-04-12T21:02:21+00:00" }, + { + "name": "phar-io/composer-distributor", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/phar-io/composer-distributor.git", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/composer-distributor/zipball/dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "reference": "dd7d936290b2a42b0c64bfe08090b5c597c280c9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "ext-dom": "*", + "ext-libxml": "*", + "phar-io/filesystem": "^2.0", + "phar-io/gnupg": "^1.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "PharIo\\ComposerDistributor\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Heigl", + "email": "andreas@heigl.org", + "role": "Developer" + }, + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info", + "role": "Developer" + } + ], + "description": "Base Code for a composer plugin that installs PHAR-files", + "homepage": "https://phar.io", + "keywords": [ + "bin", + "binary", + "composer", + "distribute", + "phar", + "phive" + ], + "support": { + "issues": "https://github.com/phar-io/composer-distributor/issues", + "source": "https://github.com/phar-io/composer-distributor/tree/1.0.2" + }, + "funding": [ + { + "url": "https://phar.io", + "type": "other" + } + ], + "time": "2023-05-31T17:05:49+00:00" + }, + { + "name": "phar-io/executor", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/executor.git", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/executor/zipball/5bfb7400224a0c1cf83343660af85c7f5a073473", + "reference": "5bfb7400224a0c1cf83343660af85c7f5a073473", + "shasum": "" + }, + "require": { + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/executor/issues", + "source": "https://github.com/phar-io/executor/tree/1.0.1" + }, + "time": "2020-11-30T10:53:57+00:00" + }, + { + "name": "phar-io/filesystem", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/filesystem.git", + "reference": "222e3ea432262a05706b7066697c21257664d9d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/filesystem/zipball/222e3ea432262a05706b7066697c21257664d9d1", + "reference": "222e3ea432262a05706b7066697c21257664d9d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/phar-io/filesystem/issues", + "source": "https://github.com/phar-io/filesystem/tree/2.0.1" + }, + "time": "2020-11-30T10:16:22+00:00" + }, + { + "name": "phar-io/gnupg", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/gnupg.git", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/gnupg/zipball/ed8ab1740ac4e9db99500e7252911f2821357093", + "reference": "ed8ab1740ac4e9db99500e7252911f2821357093", + "shasum": "" + }, + "require": { + "phar-io/executor": "^1.0", + "phar-io/filesystem": "^2.0", + "php": "^7.2||^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + } + ], + "description": "Thin GnuPG wrapper class around the gnupg binary, mimicking the pecl/gnupg api", + "support": { + "issues": "https://github.com/phar-io/gnupg/issues", + "source": "https://github.com/phar-io/gnupg/tree/1.0.3" + }, + "time": "2024-08-22T20:45:57+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1594,6 +1808,42 @@ }, "time": "2024-04-09T21:13:58+00:00" }, + { + "name": "phpdocumentor/shim", + "version": "v3.9.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/shim.git", + "reference": "f34a43193996194a2b22d243b650f1fbf5746257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/shim/zipball/f34a43193996194a2b22d243b650f1fbf5746257", + "reference": "f34a43193996194a2b22d243b650f1fbf5746257", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "phar-io/composer-distributor": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "phpDocumentor\\Plugin" + }, + "autoload": { + "psr-4": { + "phpDocumentor\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "support": { + "source": "https://github.com/phpDocumentor/shim/tree/v3.9.1" + }, + "time": "2025-11-25T21:55:20+00:00" + }, { "name": "phpdocumentor/type-resolver", "version": "1.8.2", @@ -5672,16 +5922,16 @@ }, { "name": "symfony/security-bundle", - "version": "v6.4.7", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "c9112933215b9b3c48851eb6644263d5c9d93245" + "reference": "50007f4f76632741b62fa9604c5f65807f268b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/c9112933215b9b3c48851eb6644263d5c9d93245", - "reference": "c9112933215b9b3c48851eb6644263d5c9d93245", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/50007f4f76632741b62fa9604c5f65807f268b72", + "reference": "50007f4f76632741b62fa9604c5f65807f268b72", "shasum": "" }, "require": { @@ -5764,7 +6014,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.7" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.10" }, "funding": [ { @@ -5780,7 +6030,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-07-17T10:49:44+00:00" }, { "name": "symfony/security-core", @@ -9473,31 +9723,31 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.59.1", + "version": "v1.65.1", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "b87b1b25c607a8a50832395bc751c784946a0350" + "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/b87b1b25c607a8a50832395bc751c784946a0350", - "reference": "b87b1b25c607a8a50832395bc751c784946a0350", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/eba30452d212769c9a5bcf0716959fd8ba1e54e3", + "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/doctrine-bundle": "<2.10", @@ -9505,12 +9755,14 @@ }, "require-dev": { "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/doctrine-bundle": "^2.5.0|^3.0.0", "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", @@ -9545,7 +9797,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.59.1" + "source": "https://github.com/symfony/maker-bundle/tree/v1.65.1" }, "funding": [ { @@ -9556,12 +9808,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-06T03:59:59+00:00" + "time": "2025-12-02T07:14:37+00:00" }, { "name": "symfony/phpunit-bridge", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..7e2aaa3 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,23 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + custom_authenticator: App\Security\AppCustomAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,7 +31,7 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } when@test: @@ -33,7 +42,7 @@ when@test: # are not important, waste resources and increase test times. The following # reduces the work factor to the lowest possible values. Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto + algorithm: bcrypt cost: 4 # Lowest possible value for bcrypt time_cost: 3 # Lowest possible value for argon memory_cost: 10 # Lowest possible value for argon diff --git a/migrations/Version20260123101220.php b/migrations/Version20260123101220.php new file mode 100644 index 0000000..2a927e2 --- /dev/null +++ b/migrations/Version20260123101220.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_IDENTIFIER_USERNAME (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c4bfed..4332f12 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/src/Controller/AccueilController.php b/src/Controller/AccueilController.php index 653490b..0a20a8e 100644 --- a/src/Controller/AccueilController.php +++ b/src/Controller/AccueilController.php @@ -19,23 +19,23 @@ class AccueilController extends AbstractController{ private $repository; /** - * + * * @param FormationRepository $repository */ public function __construct(FormationRepository $repository) { $this->repository = $repository; - } + } #[Route('/', name: 'accueil')] public function index(): Response{ $formations = $this->repository->findAllLasted(2); return $this->render("pages/accueil.html.twig", [ 'formations' => $formations - ]); + ]); } #[Route('/cgu', name: 'cgu')] public function cgu(): Response{ - return $this->render("pages/cgu.html.twig"); + return $this->render("pages/cgu.html.twig"); } } diff --git a/src/Controller/FormationsController.php b/src/Controller/FormationsController.php index e51f36b..045a371 100644 --- a/src/Controller/FormationsController.php +++ b/src/Controller/FormationsController.php @@ -16,18 +16,19 @@ use Symfony\Component\Routing\Annotation\Route; class FormationsController extends AbstractController { /** - * + * * @var FormationRepository */ private $formationRepository; /** - * + * * @var CategorieRepository */ private $categorieRepository; + private $formationPage = "/pages/formations.html.twig"; - function __construct(FormationRepository $formationRepository, CategorieRepository $categorieRepository) { + public function __construct(FormationRepository $formationRepository, CategorieRepository $categorieRepository) { $this->formationRepository = $formationRepository; $this->categorieRepository= $categorieRepository; } @@ -36,7 +37,7 @@ class FormationsController extends AbstractController { public function index(): Response{ $formations = $this->formationRepository->findAll(); $categories = $this->categorieRepository->findAll(); - return $this->render("pages/formations.html.twig", [ + return $this->render($this->formationPage, [ 'formations' => $formations, 'categories' => $categories ]); @@ -46,31 +47,31 @@ class FormationsController extends AbstractController { public function sort($champ, $ordre, $table=""): Response{ $formations = $this->formationRepository->findAllOrderBy($champ, $ordre, $table); $categories = $this->categorieRepository->findAll(); - return $this->render("pages/formations.html.twig", [ + return $this->render($this->formationPage, [ 'formations' => $formations, 'categories' => $categories ]); - } + } #[Route('/formations/recherche/{champ}/{table}', name: 'formations.findallcontain')] public function findAllContain($champ, Request $request, $table=""): Response{ $valeur = $request->get("recherche"); $formations = $this->formationRepository->findByContainValue($champ, $valeur, $table); $categories = $this->categorieRepository->findAll(); - return $this->render("pages/formations.html.twig", [ + return $this->render($this->formationPage, [ 'formations' => $formations, 'categories' => $categories, 'valeur' => $valeur, 'table' => $table ]); - } + } #[Route('/formations/formation/{id}', name: 'formations.showone')] public function showOne($id): Response{ $formation = $this->formationRepository->find($id); return $this->render("pages/formation.html.twig", [ 'formation' => $formation - ]); - } + ]); + } } diff --git a/src/Controller/PlaylistsController.php b/src/Controller/PlaylistsController.php index 1990d5f..b38c761 100644 --- a/src/Controller/PlaylistsController.php +++ b/src/Controller/PlaylistsController.php @@ -17,24 +17,25 @@ use Symfony\Component\Routing\Annotation\Route; class PlaylistsController extends AbstractController { /** - * + * * @var PlaylistRepository */ private $playlistRepository; /** - * + * * @var FormationRepository */ private $formationRepository; /** - * + * * @var CategorieRepository */ - private $categorieRepository; + private $categorieRepository; + private $playlistPage = "/pages/playlists.html.twig"; - function __construct(PlaylistRepository $playlistRepository, + public function __construct(PlaylistRepository $playlistRepository, CategorieRepository $categorieRepository, FormationRepository $formationRespository) { $this->playlistRepository = $playlistRepository; @@ -50,9 +51,16 @@ class PlaylistsController extends AbstractController { public function index(): Response{ $playlists = $this->playlistRepository->findAllOrderByName('ASC'); $categories = $this->categorieRepository->findAll(); + + $nombreFormations = []; + foreach ($playlists as $p) { + $nombreFormations[$p->getId()] = $this->playlistRepository->countFormationsByPlaylist($p); + } + return $this->render("pages/playlists.html.twig", [ 'playlists' => $playlists, - 'categories' => $categories + 'categories' => $categories, + 'nombreFormations' => $nombreFormations ]); } @@ -62,37 +70,57 @@ class PlaylistsController extends AbstractController { case "name": $playlists = $this->playlistRepository->findAllOrderByName($ordre); break; + case "nbResult": + $playlists = $this->playlistRepository->findAllOrderByResultNb($ordre); + break; + default: + break; } $categories = $this->categorieRepository->findAll(); - return $this->render("pages/playlists.html.twig", [ + + $nombreFormations = []; + foreach ($playlists as $p) { + $nombreFormations[$p->getId()] = $this->playlistRepository->countFormationsByPlaylist($p); + } + + return $this->render($this->playlistPage, [ 'playlists' => $playlists, - 'categories' => $categories + 'categories' => $categories, + 'nombreFormations' => $nombreFormations ]); - } + } #[Route('/playlists/recherche/{champ}/{table}', name: 'playlists.findallcontain')] public function findAllContain($champ, Request $request, $table=""): Response{ $valeur = $request->get("recherche"); $playlists = $this->playlistRepository->findByContainValue($champ, $valeur, $table); $categories = $this->categorieRepository->findAll(); - return $this->render("pages/playlists.html.twig", [ + + $nombreFormations = []; + foreach ($playlists as $p) { + $nombreFormations[$p->getId()] = $this->playlistRepository->countFormationsByPlaylist($p); + } + + return $this->render($this->playlistPage, [ 'playlists' => $playlists, - 'categories' => $categories, + 'categories' => $categories, 'valeur' => $valeur, - 'table' => $table + 'table' => $table, + 'nombreFormations' => $nombreFormations ]); - } + } #[Route('/playlists/playlist/{id}', name: 'playlists.showone')] public function showOne($id): Response{ $playlist = $this->playlistRepository->find($id); $playlistCategories = $this->categorieRepository->findAllForOnePlaylist($id); $playlistFormations = $this->formationRepository->findAllForOnePlaylist($id); + $nbFormations = $this->playlistRepository->countFormationsByPlaylist($playlist); return $this->render("pages/playlist.html.twig", [ 'playlist' => $playlist, 'playlistcategories' => $playlistCategories, - 'playlistformations' => $playlistFormations - ]); - } - + 'playlistformations' => $playlistFormations, + 'nombreFormations' => $nbFormations + ]); + } } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..2133610 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,32 @@ +getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Controller/admin/AdminCategoriesController.php b/src/Controller/admin/AdminCategoriesController.php new file mode 100644 index 0000000..c82a6a5 --- /dev/null +++ b/src/Controller/admin/AdminCategoriesController.php @@ -0,0 +1,67 @@ +categorieRepository = $categorieRepository; + } + + #[Route('/admin/categories', name:'admin.categories')] + public function index(){ + $categories = $this->categorieRepository->findAll(); + $count = []; + foreach ($categories as $categorie) { + $count[$categorie->getId()] = $this->categorieRepository->countFormationsByCategorie($categorie); + } + + return $this->render('pages/admin/admin.categories.html.twig', [ + 'categories'=> $categories, + 'usageCounts' => $count, + ]); + } + + #[Route('/admin/categories/remove/{id}', name:'admin.categories.remove')] + public function remove(int $id){ + $categorie = $this->categorieRepository->find($id); + $count = $this->categorieRepository->countFormationsByCategorie($categorie); + + if($count == 0){ + $this->categorieRepository->remove($categorie); + return $this->redirectToRoute('admin.categories'); + } + return $this->redirectToRoute('admin.categories'); + } + + #[Route('admin/categories/add', name:'admin.categories.add')] + public function add(Request $request){ + $name = $request->request->get('name'); + $token = $request->request->get('_token'); + + if (!$this->isCsrfTokenValid('filtre_title', $token)) { + throw $this->createAccessDeniedException('Token CSRF invalide.'); + } + + if ($name) { + $category = new Categorie(); + $category->setName($name); + + $this->categorieRepository->add($category); + + $this->addFlash('success', 'Catégorie ajoutée !'); + } + return $this->redirectToRoute('admin.categories'); + } +} \ No newline at end of file diff --git a/src/Controller/admin/AdminFormationsController.php b/src/Controller/admin/AdminFormationsController.php new file mode 100644 index 0000000..fe820b2 --- /dev/null +++ b/src/Controller/admin/AdminFormationsController.php @@ -0,0 +1,105 @@ +formationRepository = $formationRepository; + $this->categorieRepository = $categorieRepository; + } + + #[Route('/admin', name: 'admin.formations')] + public function index() : Response + { + $formations = $this->formationRepository->findAllOrderBy('publishedAt', 'DESC'); + $categories = $this->categorieRepository->findAll(); + return $this->render("pages/admin/admin.formations.html.twig", [ + 'formations' => $formations, + 'categories' => $categories + ]); + } + + #[Route('/admin/formations/tri/{champ}/{ordre}/{table}', name: 'admin.formations.sort')] + public function sort($champ, $ordre, $table=""): Response{ + $formations = $this->formationRepository->findAllOrderBy($champ, $ordre, $table); + $categories = $this->categorieRepository->findAll(); + return $this->render($this->adminPage, [ + 'formations' => $formations, + 'categories' => $categories + ]); + } + + #[Route('/admin/recherche/{champ}/{table}', name: 'admin.formations.findallcontain')] + public function findAllContain($champ, Request $request, $table=""): Response{ + $valeur = $request->get("recherche"); + $formations = $this->formationRepository->findByContainValue($champ, $valeur, $table); + $categories = $this->categorieRepository->findAll(); + return $this->render($this->adminPage, [ + 'formations' => $formations, + 'categories' => $categories, + 'valeur' => $valeur, + 'table' => $table + ]); + } + + #[Route('/admin/creerFormation', name: 'admin.creerFormation')] + public function afficherCreerFormation(Request $request) : Response{ + $formation = new Formation(); + $formCreateFormation = $this->createForm(FormationType::class, $formation); + $formCreateFormation->handleRequest($request); + + if($formCreateFormation->isSubmitted() && $formCreateFormation->isValid()){ + $this->formationRepository->add($formation); + $this->addFlash('success', 'La formation a bien été créée !'); + return $this->redirectToRoute('admin.formations'); + } + + return $this->render('pages/admin/admin.addFormation.html.twig', [ + 'formCreateFormation'=> $formCreateFormation->createView() + ]); + } + + #[Route('admin/formations/modifier/{id}', name: 'admin.formations.modifier')] + public function modifier(Request $request, int $id){ + $formation = $this->formationRepository->find($id); + + $formModifierFormation = $this->createForm(FormationType::class, $formation); + $formModifierFormation->handleRequest($request); + + if($formModifierFormation->isSubmitted() && $formModifierFormation->isValid()){ + $this->formationRepository->add($formation); + $this->addFlash('success', 'La formation a bien été créée !'); + return $this->redirectToRoute('admin.formations'); + } + + return $this->render('pages/admin/admin.addFormation.html.twig', [ + 'formCreateFormation'=> $formModifierFormation->createView() + ]); + } + + #[Route('/admin/remove/{id}', name: 'admin.formations.remove')] + public function remove(int $id) + { + $formation = $this->formationRepository->find($id); + $this->formationRepository->remove($formation); + return $this->redirectToRoute('admin.formations'); + } +} diff --git a/src/Controller/admin/AdminPlaylistsController.php b/src/Controller/admin/AdminPlaylistsController.php new file mode 100644 index 0000000..d59a2d7 --- /dev/null +++ b/src/Controller/admin/AdminPlaylistsController.php @@ -0,0 +1,120 @@ +playlistRepository = $playlistRepository; + $this->formationRepository = $formationRepository; + $this->categorieRepository = $categorieRepository; + } + + #[Route('/admin/playlists', name: 'admin.playlists')] + public function index() : Response + { + $playlists = $this->playlistRepository->findAllOrderByName('ASC'); + return $this->render($this->playlistPage, [ + "playlists" => $playlists, + ]); + } + + #[Route('/admin/playlists/delete/{id}', name:'admin.playlists.remove')] + public function remove($id){ + $playlist = $this->playlistRepository->find($id); + if($this->playlistRepository->countFormationsByPlaylist($playlist) > 0){ + return $this->redirectToRoute('admin.playlists'); + } + + $this->playlistRepository->remove($playlist); + return $this->redirectToRoute('admin.playlists'); + } + + #[Route('/admin/playlists/modifier/{id}', name:'admin.playlists.modifier')] + public function modifier(Request $request, int $id){ + $playlist = $this->playlistRepository->find($id); + $formModifierPlaylist = $this->createForm(PlaylistType::class, $playlist); + $formModifierPlaylist->handleRequest($request); + + $formations = $this->formationRepository->findAllForOnePlaylist($id); + + if($formModifierPlaylist->isSubmitted() && $formModifierPlaylist->isValid()){ + $this->playlistRepository->add($playlist); + $this->addFlash('success', 'La playlist a bien été modifiée !'); + return $this->redirectToRoute('admin.playlists'); + } + + return $this->render('pages/admin/admin.addPlaylist.html.twig', [ + 'formModifierPlaylist'=> $formModifierPlaylist->createView(), + 'formations'=> $formations, + ]); + } + + #[Route('/admin/playlists/create', name:'admin.playlists.create')] + public function create(Request $request){ + $playlist = new Playlist; + $formCreerPlaylist = $this->createForm(PlaylistType::class, $playlist); + $formCreerPlaylist->handleRequest($request); + + $formations = []; + + if($formCreerPlaylist->isSubmitted() && $formCreerPlaylist->isValid()){ + $this->playlistRepository->add($playlist); + $this->addFlash('success', 'La playlist a bien été créée !'); + return $this->redirectToRoute('admin.playlists'); + } + + return $this->render('pages/admin/admin.addPlaylist.html.twig', [ + 'formModifierPlaylist'=> $formCreerPlaylist->createView(), + 'formations'=> $formations, + ]); + } + + #[Route('/admin/playlists/sort/{champ}/{ordre}', name:'admin.playlists.sort')] + public function sort(string $champ, string $ordre){ + // + $playlists = $this->playlistRepository->findAllOrderByName($ordre); + return $this->render($this->playlistPage, [ + "playlists" => $playlists, + ]); + } + + #[Route('/admin/playlists/recherche/{champ}/{table}', name: 'adminplaylists.findallcontain')] + public function findAllContain($champ, Request $request, $table=""): Response{ + $valeur = $request->get("recherche"); + $playlists = $this->playlistRepository->findByContainValue($champ, $valeur, $table); + $categories = $this->categorieRepository->findAll(); + + $nombreFormations = []; + foreach ($playlists as $p) { + $nombreFormations[$p->getId()] = $this->playlistRepository->countFormationsByPlaylist($p); + } + + return $this->render($this->playlistPage, [ + 'playlists' => $playlists, + 'categories' => $categories, + 'valeur' => $valeur, + 'table' => $table, + 'nombreFormations' => $nombreFormations + ]); + } +} \ No newline at end of file diff --git a/src/Entity/Formation.php b/src/Entity/Formation.php index 311de22..5d09d5e 100644 --- a/src/Entity/Formation.php +++ b/src/Entity/Formation.php @@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: FormationRepository::class)] class Formation @@ -15,7 +16,7 @@ class Formation /** * Début de chemin vers les images */ - private const cheminImage = "https://i.ytimg.com/vi/"; + private const CHEMINIMAGE = "https://i.ytimg.com/vi/"; #[ORM\Id] #[ORM\GeneratedValue] @@ -23,6 +24,7 @@ class Formation private ?int $id = null; #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + #[Assert\LessThanOrEqual('now', message:"La date ne peut pas être postétrieure à aujourd'hui.")] private ?\DateTimeInterface $publishedAt = null; #[ORM\Column(length: 100, nullable: true)] @@ -69,8 +71,8 @@ class Formation if($this->publishedAt == null){ return ""; } - return $this->publishedAt->format('d/m/Y'); - } + return $this->publishedAt->format('d/m/Y'); + } public function getTitle(): ?string { @@ -110,12 +112,12 @@ class Formation public function getMiniature(): ?string { - return self::cheminImage.$this->videoId."/default.jpg"; + return self::CHEMINIMAGE.$this->videoId."/default.jpg"; } public function getPicture(): ?string { - return self::cheminImage.$this->videoId."/hqdefault.jpg"; + return self::CHEMINIMAGE.$this->videoId."/hqdefault.jpg"; } public function getPlaylist(): ?playlist diff --git a/src/Entity/Playlist.php b/src/Entity/Playlist.php index e1fe529..a1688e3 100644 --- a/src/Entity/Playlist.php +++ b/src/Entity/Playlist.php @@ -107,5 +107,13 @@ class Playlist } return $categories; } + + /** + * @return int + */ + public function getCountFormation() : int + { + return $this->formations->count(); + } } diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..37c7259 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,103 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + #[\Deprecated] + public function eraseCredentials(): void + { + // @deprecated, to be removed when upgrading to Symfony 8 + } +} diff --git a/src/Form/FormationType.php b/src/Form/FormationType.php new file mode 100644 index 0000000..dd5f56b --- /dev/null +++ b/src/Form/FormationType.php @@ -0,0 +1,55 @@ +add('publishedAt', null, [ + 'widget' => 'single_text', + 'required' => 'true' + ]) + ->add('title', null, [ + 'required' => 'true' + ]) + ->add('description', null, [ + 'required' => 'false' + ]) + ->add('videoId', null, [ + 'required' => 'true' + ]) + ->add('playlist', EntityType::class, [ + 'class' => Playlist::class, + 'choice_label' => 'name', + 'required' => 'true' + ]) + ->add('categories', EntityType::class, [ + 'class' => Categorie::class, + 'choice_label' => 'name', + 'multiple' => true, + 'required' => false, + ]) + ->add('submit', SubmitType::class, [ + 'label' => 'Enregistrer' + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Formation::class, + ]); + } +} diff --git a/src/Form/PlaylistType.php b/src/Form/PlaylistType.php new file mode 100644 index 0000000..8b080d2 --- /dev/null +++ b/src/Form/PlaylistType.php @@ -0,0 +1,32 @@ +add('name', null, [ + 'required' => true + ]) + ->add('description') + ->add('submit', SubmitType::class, [ + 'label' => 'Enregistrer' + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Playlist::class, + ]); + } +} diff --git a/src/Repository/CategorieRepository.php b/src/Repository/CategorieRepository.php index 771c2e6..0a25183 100644 --- a/src/Repository/CategorieRepository.php +++ b/src/Repository/CategorieRepository.php @@ -39,9 +39,25 @@ class CategorieRepository extends ServiceEntityRepository ->join('f.playlist', 'p') ->where('p.id=:id') ->setParameter('id', $idPlaylist) - ->orderBy('c.name', 'ASC') + ->orderBy('c.name', 'ASC') ->getQuery() - ->getResult(); - } + ->getResult(); + } + + /** + * Compte le nombre de formations pour une catégorie donnée + * @param Categorie $categorie + * @return int + */ + public function countFormationsByCategorie(Categorie $categorie): int + { + return (int) $this->createQueryBuilder('c') + ->select('COUNT(f.id)') + ->join('c.formations', 'f') + ->where('c.id = :id') + ->setParameter('id', $categorie->getId()) + ->getQuery() + ->getSingleScalarResult(); + } } diff --git a/src/Repository/FormationRepository.php b/src/Repository/FormationRepository.php index 4d7b173..a582770 100644 --- a/src/Repository/FormationRepository.php +++ b/src/Repository/FormationRepository.php @@ -31,11 +31,11 @@ class FormationRepository extends ServiceEntityRepository /** * Retourne toutes les formations triées sur un champ * @param type $champ - * @param type $ordre + * @param string $ordre * @param type $table si $champ dans une autre table * @return Formation[] */ - public function findAllOrderBy($champ, $ordre, $table=""): array{ + public function findAllOrderBy($champ, string $ordre, $table=""): array{ if($table==""){ return $this->createQueryBuilder('f') ->orderBy('f.'.$champ, $ordre) @@ -46,7 +46,7 @@ class FormationRepository extends ServiceEntityRepository ->join('f.'.$table, 't') ->orderBy('t.'.$champ, $ordre) ->getQuery() - ->getResult(); + ->getResult(); } } @@ -68,30 +68,30 @@ class FormationRepository extends ServiceEntityRepository ->orderBy('f.publishedAt', 'DESC') ->setParameter('valeur', '%'.$valeur.'%') ->getQuery() - ->getResult(); + ->getResult(); }else{ return $this->createQueryBuilder('f') - ->join('f.'.$table, 't') + ->join('f.'.$table, 't') ->where('t.'.$champ.' LIKE :valeur') ->orderBy('f.publishedAt', 'DESC') ->setParameter('valeur', '%'.$valeur.'%') ->getQuery() - ->getResult(); - } - } + ->getResult(); + } + } /** * Retourne les n formations les plus récentes - * @param type $nb + * @param int $nb * @return Formation[] */ - public function findAllLasted($nb) : array { + public function findAllLasted(int $nb) : array { return $this->createQueryBuilder('f') ->orderBy('f.publishedAt', 'DESC') - ->setMaxResults($nb) + ->setMaxResults($nb) ->getQuery() ->getResult(); - } + } /** * Retourne la liste des formations d'une playlist @@ -103,9 +103,9 @@ class FormationRepository extends ServiceEntityRepository ->join('f.playlist', 'p') ->where('p.id=:id') ->setParameter('id', $idPlaylist) - ->orderBy('f.publishedAt', 'ASC') + ->orderBy('f.publishedAt', 'ASC') ->getQuery() - ->getResult(); + ->getResult(); } } diff --git a/src/Repository/PlaylistRepository.php b/src/Repository/PlaylistRepository.php index 49ddc93..cb56c06 100644 --- a/src/Repository/PlaylistRepository.php +++ b/src/Repository/PlaylistRepository.php @@ -30,8 +30,7 @@ class PlaylistRepository extends ServiceEntityRepository /** * Retourne toutes les playlists triées sur le nom de la playlist - * @param type $champ - * @param type $ordre + * @param string $ordre * @return Playlist[] */ public function findAllOrderByName($ordre): array{ @@ -40,22 +39,22 @@ class PlaylistRepository extends ServiceEntityRepository ->groupBy('p.id') ->orderBy('p.name', $ordre) ->getQuery() - ->getResult(); - } + ->getResult(); + } /** * Enregistrements dont un champ contient une valeur * ou tous les enregistrements si la valeur est vide - * @param type $champ - * @param type $valeur - * @param type $table si $champ dans une autre table + * @param string $champ + * @param string $valeur + * @param string $table si $champ dans une autre table * @return Playlist[] */ public function findByContainValue($champ, $valeur, $table=""): array{ if($valeur==""){ return $this->findAllOrderByName('ASC'); - } - if($table==""){ + } + if($table==""){ return $this->createQueryBuilder('p') ->leftjoin('p.formations', 'f') ->where('p.'.$champ.' LIKE :valeur') @@ -63,8 +62,8 @@ class PlaylistRepository extends ServiceEntityRepository ->groupBy('p.id') ->orderBy('p.name', 'ASC') ->getQuery() - ->getResult(); - }else{ + ->getResult(); + }else{ return $this->createQueryBuilder('p') ->leftjoin('p.formations', 'f') ->leftjoin('f.categories', 'c') @@ -73,8 +72,40 @@ class PlaylistRepository extends ServiceEntityRepository ->groupBy('p.id') ->orderBy('p.name', 'ASC') ->getQuery() - ->getResult(); - } - } - + ->getResult(); + } + } + + /** + * Retourne toutes les playlists triées sur le nombre de résultats par playlist + * @param string $ordre + * @return Playlist[] + */ + public function findAllOrderByResultNb($ordre){ + return $this->createQueryBuilder('p') + ->select('p', 'COUNT(f.id) AS HIDDEN nbrFormations') + ->leftJoin('p.formations', 'f') + ->groupBy('p.id') + ->orderBy('nbrFormations', $ordre) + ->getQuery() + ->getResult(); + } + + /** + * Renvoie le nombre de formations pour une playlist donnée + * @param Playlist $playlist + * @return int + */ + public function countFormationsByPlaylist(Playlist $playlist): int + { + $nb = $this->createQueryBuilder('p') + ->select('COUNT(f.id)') + ->leftJoin('p.formations', 'f') + ->where('p = :playlist') + ->setParameter('playlist', $playlist) + ->getQuery() + ->getSingleScalarResult(); + + return (int) $nb; + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/AppCustomAuthenticator.php b/src/Security/AppCustomAuthenticator.php new file mode 100644 index 0000000..eb0bb08 --- /dev/null +++ b/src/Security/AppCustomAuthenticator.php @@ -0,0 +1,54 @@ +getPayload()->getString('username'); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $username); + + return new Passport( + new UserBadge($username), + new PasswordCredentials($request->getPayload()->getString('password')), + [ + new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response +{ + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + return new RedirectResponse($this->urlGenerator->generate('app_home')); +} + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/templates/baseadmin.html.twig b/templates/baseadmin.html.twig new file mode 100644 index 0000000..6405dd7 --- /dev/null +++ b/templates/baseadmin.html.twig @@ -0,0 +1,43 @@ +{% extends "base.html.twig" %} + +{% block title %}{% endblock %} +{% block stylesheets %}{% endblock %} +{% block top %} +
+ +
+ Bannière Mediatek Formation +
+ + +
+{% endblock %} +{% block body %}{% endblock %} +{% block footer %} +
+ +
+{% endblock %} +{% block javascripts %}{% endblock %} \ No newline at end of file diff --git a/templates/basefront.html.twig b/templates/basefront.html.twig index fcb998e..e7e87e1 100644 --- a/templates/basefront.html.twig +++ b/templates/basefront.html.twig @@ -6,7 +6,7 @@
- + Bannière Mediatek Formation
- + catégories diff --git a/templates/pages/playlist.html.twig b/templates/pages/playlist.html.twig index f68e297..22ffb0a 100644 --- a/templates/pages/playlist.html.twig +++ b/templates/pages/playlist.html.twig @@ -11,7 +11,8 @@ {% endfor %}

description :
- {{ playlist.description|nl2br }} + {{ playlist.description|nl2br }}
+ Nombre de formations : {{ nombreFormations }}
@@ -20,7 +21,7 @@
{% if formation.miniature %} - + Miniature de la formation {% endif %}
diff --git a/templates/pages/playlists.html.twig b/templates/pages/playlists.html.twig index 68d7eb3..61c3643 100644 --- a/templates/pages/playlists.html.twig +++ b/templates/pages/playlists.html.twig @@ -33,7 +33,9 @@ -   + Nb résultats + < + > @@ -57,7 +59,10 @@ Voir détail - + + + {{ nombreFormations[playlists[k].id] ?? 0 }} + {% endfor %} {% endif %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..1b0c67d --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,28 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ You are logged in as {{ app.user.userIdentifier }}, Logout +
+ {% endif %} + +

Please sign in

+ + + + + + + +
+{% endblock %}