From f8a1bcab718dd6c0757b4696e24e27faed7f7d24 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Tue, 1 Jul 2025 19:07:59 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20d'une=20th=C3=A9matisation=20des=20even?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- Makefile | 2 +- app/config/config.php | 32 --- app/config/config.yml | 37 +-- app/config/config_dev.yml | 1 + app/config/config_prod.yml | 3 + app/config/routing/admin_event.yml | 19 ++ app/config/security.yml | 2 +- app/config/services.yml | 34 +-- bin/console | 2 + compose.yml | 1 + composer.json | 6 +- composer.lock | 80 +++++- ...0250701085600_add_event_themes_feature.php | 30 +++ htdocs/app.php | 11 +- .../pages/administration/forum_sessions.php | 17 +- sources/Afup/Forum/AppelConferencier.php | 4 + sources/Afup/Utils/SymfonyKernel.php | 2 + .../Command/UpdateMailchimpMembersCommand.php | 2 +- .../Admin/Event/EventThemeAction.php | 96 ++++++++ .../Admin/Event/EventThemeAddEditAction.php | 56 +++++ .../Admin/TechLetter/GenerateAction.php | 2 +- .../Admin/TechLetter/SendTestAction.php | 2 +- .../Controller/Event/Blog/ProgramAction.php | 11 +- .../AppBundle/Controller/LegacyController.php | 2 + .../Controller/Website/Global/HomeAction.php | 2 +- .../Controller/Website/Meetups/ListAction.php | 4 +- .../Website/NewsletterController.php | 2 +- .../Website/Static/SuperAperoAction.php | 2 +- .../Controller/Website/Talks/ListAction.php | 4 +- .../Website/Techletter/WebhookAction.php | 2 +- .../Website/TechnoWatch/CalendarAction.php | 4 +- .../AppBundle/Event/Form/EventThemeType.php | 64 +++++ sources/AppBundle/Event/Form/EventType.php | 4 + sources/AppBundle/Event/Model/Event.php | 15 ++ sources/AppBundle/Event/Model/EventTheme.php | 86 +++++++ .../Model/Repository/EventRepository.php | 27 +++ .../Model/Repository/EventThemeRepository.php | 23 ++ .../Event/Model/Repository/TalkRepository.php | 46 +++- sources/AppBundle/Event/Model/Talk.php | 13 + sources/AppBundle/Mailchimp/Runner.php | 2 +- sources/AppBundle/Notifier/SlackNotifier.php | 2 +- sources/AppBundle/Slack/UsersClient.php | 4 +- sources/AppBundle/Twig/TwigExtension.php | 4 +- templates/admin/event/form.html.twig | 1 + .../admin/event/theme_add_edit.html.twig | 97 ++++++++ templates/admin/event/theme_list.html.twig | 229 ++++++++++++++++++ templates/blog/program.html.twig | 21 ++ 48 files changed, 1009 insertions(+), 105 deletions(-) delete mode 100644 app/config/config.php create mode 100644 db/migrations/20250701085600_add_event_themes_feature.php create mode 100644 sources/AppBundle/Controller/Admin/Event/EventThemeAction.php create mode 100644 sources/AppBundle/Controller/Admin/Event/EventThemeAddEditAction.php create mode 100644 sources/AppBundle/Event/Form/EventThemeType.php create mode 100644 sources/AppBundle/Event/Model/EventTheme.php create mode 100644 sources/AppBundle/Event/Model/Repository/EventThemeRepository.php create mode 100644 templates/admin/event/theme_add_edit.html.twig create mode 100644 templates/admin/event/theme_list.html.twig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d6dce7af..9847b813f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,4 +139,4 @@ jobs: composer-options: "--no-scripts" - name: Rector - run: make rector + run: make rector \ No newline at end of file diff --git a/Makefile b/Makefile index e2a4492c1..59da19938 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ test-functional: data config htdocs/uploads tmp CURRENT_UID=$(CURRENT_UID) $(DOCKER_COMPOSE_BIN) stop dbtest apachephptest mailcatcher CURRENT_UID=$(CURRENT_UID) $(DOCKER_COMPOSE_BIN) up -d dbtest apachephptest mailcatcher make clean-test-deprecated-log - CURRENT_UID=$(CURRENT_UID) $(DOCKER_COMPOSE_BIN) run --no-deps --rm -u localUser apachephp ./bin/behat + CURRENT_UID=$(CURRENT_UID) $(DOCKER_COMPOSE_BIN) run --no-deps --rm -u localUser apachephp ./bin/behat --colors make var/logs/test.deprecations_grouped.log CURRENT_UID=$(CURRENT_UID) $(DOCKER_COMPOSE_BIN) stop dbtest apachephptest mailcatcher diff --git a/app/config/config.php b/app/config/config.php deleted file mode 100644 index abe8a5af9..000000000 --- a/app/config/config.php +++ /dev/null @@ -1,32 +0,0 @@ -setParameter($name, $value); -} diff --git a/app/config/config.yml b/app/config/config.yml index 3935ccc36..ef9d75497 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -1,5 +1,4 @@ imports: - - { resource: config.php } - { resource: security.yml } - { resource: services.yml } - { resource: packages/http_client.yaml } @@ -160,6 +159,14 @@ parameters: forum_sessions: nom: 'Conférences' niveau: 'ROLE_FORUM' + admin_event_themes_list: + nom: 'Thèmes' + niveau: 'ROLE_FORUM' + url: '/admin/event/themes' + extra_routes: + - admin_event_themes_list + - admin_event_themes_add + - admin_event_themes_edit forum_vote_github: nom: 'Votes visiteurs' niveau: 'ROLE_FORUM' @@ -315,7 +322,7 @@ framework: fallbacks: ["%locale%"] enabled: true default_path: "%kernel.project_dir%/../translations" - secret: "%secret%" + secret: "%env(SECRET)%" router: resource: "%kernel.project_dir%/config/routing.yml" strict_requirements: ~ @@ -351,7 +358,7 @@ twig: strict_variables: "%kernel.debug%" globals: photo_storage: '@AppBundle\CFP\PhotoStorage' - global_menu_event_label: '%afup_global_menu_event_label%' + global_menu_event_label: '%env(AFUP_GLOBAL_MENU_EVENT_LABEL)%' form_themes: ['form_theme.html.twig'] exception_controller: null default_path: "%kernel.project_dir%/../templates" @@ -363,9 +370,9 @@ ting: charset: utf8mb4 master: host: "%database_host%" - port: "%database_port%" - user: "%database_user%" - password: "%database_password%" + port: "%env(int:DATABASE_PORT)%" + user: "%env(DATABASE_USER)%" + password: "%env(DATABASE_PASSWORD)%" repositories: event: @@ -374,14 +381,14 @@ ting: options: default: connection: main - database: '%database_name%' + database: '%env(DATABASE_NAME)%' site: namespace : AppBundle\Site\Model\Repository directory : "@AppBundle/Site/Model/Repository" options: default: connection: main - database: '%database_name%' + database: '%env(DATABASE_NAME)%' association: namespace : AppBundle\Association\Model\Repository @@ -389,35 +396,35 @@ ting: options: default: connection: main - database: '%database_name%' + database: '%env(DATABASE_NAME)%' throttling: namespace : AppBundle\Security\ActionThrottling directory : "@AppBundle/Security/ActionThrottling" options: default: connection: main - database: '%database_name%' + database: '%env(DATABASE_NAME)%' techletter: namespace: AppBundle\TechLetter\Model\Repository directory: "@AppBundle/TechLetter/Model/Repository" options: default: connection: main - database: "%database_name%" + database: "%env(DATABASE_NAME)%" knpu_oauth2_client: clients: # will create a service: knpu.oauth2.client.facebook_main github_main: type: github - client_id: "%github_client_id%" - client_secret: "%github_client_secret%" + client_id: "%env(GITHUB_CLIENT_ID)%" + client_secret: "%env(GITHUB_CLIENT_SECRET)%" # see below redirect_route: connection_github_check ewz_recaptcha: - public_key: '%recaptcha_public_key%' - private_key: '%recaptcha_private_key%' + public_key: '%env(RECAPTCHA_PUBLIC_KEY)%' + private_key: '%env(RECAPTCHA_PRIVATE_KEY)%' ekino_new_relic: diff --git a/app/config/config_dev.yml b/app/config/config_dev.yml index e68908a2a..d736d56a0 100644 --- a/app/config/config_dev.yml +++ b/app/config/config_dev.yml @@ -33,6 +33,7 @@ monolog: parameters: paybox_ips: [127.0.0.1, 192.168.42.1] + database_host: "db" #swiftmailer: # delivery_address: me@example.com diff --git a/app/config/config_prod.yml b/app/config/config_prod.yml index f9e8f2a04..e468aa315 100644 --- a/app/config/config_prod.yml +++ b/app/config/config_prod.yml @@ -1,6 +1,9 @@ imports: - { resource: config.yml } +parameters: + database_host: "%env(DATABASE_HOST)%" + #doctrine: # orm: # metadata_cache_driver: apc diff --git a/app/config/routing/admin_event.yml b/app/config/routing/admin_event.yml index 86e76fa97..d0da346b3 100644 --- a/app/config/routing/admin_event.yml +++ b/app/config/routing/admin_event.yml @@ -105,3 +105,22 @@ admin_event_restore: admin_event_votes: path: /votes defaults: {_controller: AppBundle\Controller\Admin\Event\VotesListeAction} + +admin_event_themes_add: + path: /themes/add + defaults: + _controller: AppBundle\Controller\Admin\Event\EventThemeAddEditAction + id: null + +admin_event_themes_edit: + path: /themes/edit/{id} + defaults: {_controller: AppBundle\Controller\Admin\Event\EventThemeAddEditAction} + requirements: + id: \d+ + +admin_event_themes_list: + path: /themes/ + requirements: + id: \d+ + defaults: + _controller: AppBundle\Controller\Admin\Event\EventThemeAction diff --git a/app/config/security.yml b/app/config/security.yml index df30ff187..0c6d17012 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -66,7 +66,7 @@ security: - { path: ^/admin/(members/reporting|association/relances|talk|slackmembers/check|healthcheck), roles: ROLE_ADMIN} - { path: ^/member, roles: [ROLE_USER, ROLE_MEMBER_EXPIRED]} - { path: ^/admin/, roles: ROLE_MEMBER_EXPIRED } - - { path: ^/blog, allow_if: "request.getClientIp() in ['217.70.189.71', '127.0.0.1', '192.168.42.1'] or request.server.get('ALLOW_BLOG_FROM_ALL') == 1 or request.headers.get('x-afup-blog-api-key') == '%blog_api_key%'" } + - { path: ^/blog, allow_if: "request.getClientIp() in ['217.70.189.71', '127.0.0.1', '192.168.42.1'] or request.server.get('ALLOW_BLOG_FROM_ALL') == 1 or request.headers.get('x-afup-blog-api-key') == '%env(BLOG_API_KEY)%'" } - { path: ^/blog, roles: ROLE_NO_ACCESS } - { path: ^/(event/\w+/tickets|association)paybox-callback, roles: PUBLIC_ACCESS, ips: "%paybox_ips%" } - { path: ^/(event/\w+/tickets|association)paybox-callback, roles: ROLE_SUPER_ADMIN } diff --git a/app/config/services.yml b/app/config/services.yml index 85ab8426d..3125645ae 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -4,8 +4,12 @@ parameters: app.badge_dir: "%kernel.project_dir%/../htdocs/uploads/badges" app.members_logo_dir: "%kernel.project_dir%/../htdocs/uploads/members_logo" app.general_meetings_dir: "%kernel.project_dir%/../htdocs/uploads/general_meetings_reports" - bluesky.api.identifier: "%bluesky_api_identifier%" - bluesky.api.app_password: "%bluesky_api_app_password%" + bluesky.api.identifier: "%env(BLUESKY_API_IDENTIFIER)%" + bluesky.api.app_password: "%env(BLUESKY_API_APP_PASSWORD)%" + database_name: "%env(DATABASE_NAME)%" + database_user: "%env(DATABASE_USER)%" + database_password: "%env(DATABASE_PASSWORD)%" + database_port: "%env(DATABASE_PORT)%" services: # service_name: @@ -25,9 +29,9 @@ services: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: public: false arguments: - - 'mysql:host=%database_host%;port=%database_port%;dbname=%database_name%' - - db_username: '%database_user%' - db_password: '%database_password%' + - 'mysql:host=%database_host%;port=%env(int:DATABASE_PORT)%;dbname=%env(DATABASE_NAME)%' + - db_username: '%env(DATABASE_USER)%' + db_password: '%env(DATABASE_PASSWORD)%' lock_mode: !php/const Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::LOCK_NONE AppBundle\: @@ -78,19 +82,19 @@ services: AppBundle\Payment\PayboxFactory: autowire: true - arguments: ["@router", "%paybox_domain_server%", "%paybox_secret_key%", "%paybox_site%", "%paybox_rang%", "%paybox_identifiant%"] + arguments: ["@router", "%env(PAYBOX_DOMAIN_SERVER)%", "%env(PAYBOX_SECRET_KEY)%", "%env(PAYBOX_SITE)%", "%env(PAYBOX_RANG)%", "%env(PAYBOX_IDENTIFIANT)%"] AppBundle\Slack\LegacyClient: - arguments: ["%slack_members_legacy_token%"] + arguments: ["%env(SLACK_MEMBERS_LEGACY_TOKEN)%"] Algolia\AlgoliaSearch\SearchClient: factory: [ Algolia\AlgoliaSearch\SearchClient, create ] - arguments: ["%algolia_app_id%", "%algolia_backend_api_key%"] + arguments: ["%env(ALGOLIA_APP_ID)%", "%env(ALGOLIA_BACKEND_API_KEY)%"] # API/Client Meetup techletter app.mailchimp_techletter_client: class: DrewM\MailChimp\MailChimp - arguments: ["%mailchimp_techletter_api_key%"] + arguments: ["%env(MAILCHIMP_TECHLETTER_API_KEY)%"] public: false app.mailchimp_techletter_api: class: AppBundle\Mailchimp\Mailchimp @@ -100,7 +104,7 @@ services: # API/Client Meetup app.mailchimp_client: class: DrewM\MailChimp\MailChimp - arguments: ["%mailchimp_api_key%"] + arguments: ["%env(MAILCHIMP_API_KEY)%"] public: false app.mailchimp_api: class: AppBundle\Mailchimp\Mailchimp @@ -111,13 +115,13 @@ services: autowire: true arguments: $mailchimp: '@app.mailchimp_techletter_api' - $listId: "%mailchimp_techletter_list%" + $listId: "%env(MAILCHIMP_TECHLETTER_LIST)%" AppBundle\Mailchimp\MailchimpMembersAutoListSynchronizer: autowire: true arguments: $mailchimp: '@app.mailchimp_api' - $listId: "%mailchimp_members_list%" + $listId: "%env(MAILCHIMP_MEMBERS_LIST)%" Afup\Site\Utils\Configuration: autowire: true @@ -130,7 +134,7 @@ services: AppBundle\Mailchimp\EventEventSubscriber: arguments: - '@app.mailchimp_api' - - "%mailchimp_members_list%" + - "%env(MAILCHIMP_MEMBERS_LIST)%" tags: - { name: kernel.event_listener, event: user.disabled, method: onUserDisabled } @@ -140,7 +144,7 @@ services: AppBundle\Event\Ticket\QrCodeGenerator: autowire: true - arguments: ["%qr_code_salt%"] + arguments: ["%env(QR_CODE_SALT)%"] Afup\Site\Forum\AppelConferencier: class: Afup\Site\Forum\AppelConferencier @@ -185,7 +189,7 @@ services: geocoder_provider_google_maps: class: Geocoder\Provider\GoogleMaps\GoogleMaps - arguments: ['@Psr\Http\Client\ClientInterface', null, "%google_maps_api_key%"] + arguments: ['@Psr\Http\Client\ClientInterface', null, "%env(GOOGLE_MAPS_API_KEY)%"] AppBundle\Offices\OfficeFinder: arguments: ['@geocoder'] diff --git a/bin/console b/bin/console index 1f510065b..f9032c4c6 100755 --- a/bin/console +++ b/bin/console @@ -3,6 +3,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Dotenv\Dotenv; // if you don't want to setup permissions the proper way, just uncomment the following PHP line // read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information @@ -16,6 +17,7 @@ $loader = require __DIR__.'/../vendor/autoload.php'; $input = new ArgvInput(); $env = $input->getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev'); $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod'; +(new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); $kernel = new AppKernel($env, $debug); $application = new Application($kernel); diff --git a/compose.yml b/compose.yml index 3b0990e04..022efe959 100644 --- a/compose.yml +++ b/compose.yml @@ -52,6 +52,7 @@ services: ENABLE_XDEBUG: ${ENABLE_XDEBUG:-false} environment: SYMFONY_ENV: "test" + APP_ENV: "test" HOST_PWD: ${PWD} SYMFONY_IDE: "%env(IDE_USED)%://open?url=file://%%f&line=%%l&/var/www/html/>%env(HOST_PWD)%/" env_file: diff --git a/composer.json b/composer.json index bb1fabcf7..85b16d2be 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "symfony/asset": "7.3.*", "symfony/clock": "7.2.*", "symfony/console": "7.3.*", + "symfony/dotenv": "7.3.*", "symfony/expression-language": "7.3.*", "symfony/form": "7.3.*", "symfony/framework-bundle": "7.3.*", @@ -99,7 +100,10 @@ "symfony-web-dir": "htdocs", "symfony-bin-dir": "bin", "symfony-var-dir": "var", - "symfony-tests-dir": "tests" + "symfony-tests-dir": "tests", + "symfony": { + "require": "7.3.*" + } }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 735e1c14e..97825575f 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": "83de42cafc1225e87c29944bd604621b", + "content-hash": "f8d8a6eb9bbc82fe481e0ca05d4b86d9", "packages": [ { "name": "algolia/algoliasearch-client-php", @@ -5487,6 +5487,84 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/dotenv", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "2192790a11f9e22cbcf9dc705a3ff22a5503923a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/2192790a11f9e22cbcf9dc705a3ff22a5503923a", + "reference": "2192790a11f9e22cbcf9dc705a3ff22a5503923a", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2025-07-10T08:29:33+00:00" + }, { "name": "symfony/error-handler", "version": "v7.3.0", diff --git a/db/migrations/20250701085600_add_event_themes_feature.php b/db/migrations/20250701085600_add_event_themes_feature.php new file mode 100644 index 000000000..c940b1aa6 --- /dev/null +++ b/db/migrations/20250701085600_add_event_themes_feature.php @@ -0,0 +1,30 @@ +table('afup_forum') + ->addColumn('has_themes', 'boolean', ['null' => false, 'default' => false]) + ->save() + ; + $this + ->table('afup_sessions') + ->addColumn('theme', 'integer', ['null' => true]) + ->save() + ; + $this + ->table('afup_conference_theme') + ->addColumn('id_forum', 'integer', ['null' => false, 'signed' => false]) + ->addColumn('name', 'string', ['limit' => 255]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('priority', 'integer', ['null' => false, 'default' => 0]) + ->save() + ; + } +} diff --git a/htdocs/app.php b/htdocs/app.php index b7a464913..65e586ff9 100644 --- a/htdocs/app.php +++ b/htdocs/app.php @@ -4,10 +4,15 @@ use Composer\Autoload\ClassLoader; use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Dotenv\Dotenv; $isDevEnv = isset($_ENV['SYMFONY_ENV']) && $_ENV['SYMFONY_ENV'] == 'dev'; $isTestEnv = isset($_ENV['SYMFONY_ENV']) && $_ENV['SYMFONY_ENV'] == 'test'; +/** @var ClassLoader $loader */ +$loader = require __DIR__ . '/../vendor/autoload.php'; +$env = $isDevEnv ? 'dev' : ($isTestEnv ? 'test' : 'prod'); +(new Dotenv())->bootEnv(dirname(__DIR__).'/.env', $env); if ($_SERVER['HTTP_HOST'] === 'afup.dev' || $isDevEnv || $isTestEnv) { if (!$isDevEnv && !$isTestEnv && @@ -21,14 +26,8 @@ exit('You are not allowed to access this file. Check ' . basename(__FILE__) . ' for more information.'); } - /** @var ClassLoader $loader */ - $loader = require __DIR__ . '/../vendor/autoload.php'; - $kernel = $isDevEnv ? new AppKernel('dev', true) : new AppKernel('test', true); } else { - /** @var ClassLoader $loader */ - $loader = require __DIR__ . '/../vendor/autoload.php'; - $kernel = new AppKernel('prod', false); } diff --git a/htdocs/pages/administration/forum_sessions.php b/htdocs/pages/administration/forum_sessions.php index c193b05e2..bce5ca487 100644 --- a/htdocs/pages/administration/forum_sessions.php +++ b/htdocs/pages/administration/forum_sessions.php @@ -28,6 +28,7 @@ $eventRepository = $this->eventRepository; $speakerRepository = $this->speakerRepository; $talkRepository = $this->talkRepository; +$eventThemeRepository = $this->eventThemeRepository; $forum = new Forum($bdd); $forum_appel = new AppelConferencier($bdd); @@ -166,6 +167,16 @@ $groupe[] = $formulaire->createElement('radio', 'skill', null, 'Senior', Talk::SKILL_SENIOR); $formulaire->addGroup($groupe, 'groupe_skill', "Niveau", '
', false); + $event = $eventRepository->get($_GET['id_forum']); + if ($event->getHasThemes()) { + $groupe = []; + $themes = $eventThemeRepository->getBy(['idForum' => $event->getId()]); + foreach ($themes as $theme) { + $groupe[] = $formulaire->createElement('radio', 'theme', null, $theme->getName(), $theme->getID()); + } + $formulaire->addGroup($groupe, 'groupe_theme', "Thème", '
', false); + } + $formulaire->addElement('checkbox' , 'needs_mentoring' , "Demande a bénéficier du programme d'accompagnement des jeunes speakers"); $formulaire->addElement('checkbox', 'with_workshop', "Propose un atelier"); $formulaire->addElement('textarea', 'workshop_abstract', 'Résumé de l\'atelier', ['cols' => 40, 'rows' => 15]); @@ -197,7 +208,6 @@ $formulaire->addElement('checkbox', 'has_allowed_to_sharing_with_local_offices', 'Accord pour le partage aux antennes'); $formulaire->addElement('header', null, 'Conférencier(s)'); - $event = $eventRepository->get($_GET['id_forum']); Assertion::notNull($event); $conferenciers = [null => '']; foreach ($speakerRepository->searchSpeakers($event) as $speaker) { @@ -278,12 +288,13 @@ (int) $valeurs['skill'], (int) $valeurs['needs_mentoring'], $valeurs['use_markdown'], - $valeurs['video_has_fr_subtitles'], - $valeurs['video_has_en_subtitles'], + $valeurs['video_has_fr_subtitles'] ?? null, + $valeurs['video_has_en_subtitles'] ?? null, $valeurs['date_publication']['Y'] . '-' . $valeurs['date_publication']['M'] . '-' . $valeurs['date_publication']['d'] . ' ' . $valeurs['date_publication']['H'] . ':' . $valeurs['date_publication']['i'] . ':' . $valeurs['date_publication']['s'], $valeurs['tweets'], $valeurs['transcript'], $valeurs['verbatim'], + $valeurs['theme'] ?? null, ); $forum_appel->delierSession($session_id); } diff --git a/sources/Afup/Forum/AppelConferencier.php b/sources/Afup/Forum/AppelConferencier.php index 6a708d1cb..05e25f6fd 100644 --- a/sources/Afup/Forum/AppelConferencier.php +++ b/sources/Afup/Forum/AppelConferencier.php @@ -408,6 +408,7 @@ public function modifierSession( $tweets = null, $transcript = null, $verbatim = null, + $theme_id = null, ) { $this->_bdd->executer("SET NAMES utf8mb4"); @@ -467,6 +468,9 @@ public function modifierSession( if ($verbatim !== null) { $requete .= 'verbatim = ' . $this->_bdd->echapper($verbatim) . ', '; } + if ($theme_id !== null) { + $requete .= 'theme = ' . $this->_bdd->echapper($theme_id) . ', '; + } $requete .= ' plannifie = ' . $this->_bdd->echapper($plannifie) . ' '; $requete .= ' WHERE session_id = ' . (int) $id; diff --git a/sources/Afup/Utils/SymfonyKernel.php b/sources/Afup/Utils/SymfonyKernel.php index 2fda6ec81..ad03e29da 100644 --- a/sources/Afup/Utils/SymfonyKernel.php +++ b/sources/Afup/Utils/SymfonyKernel.php @@ -4,6 +4,7 @@ namespace Afup\Site\Utils; +use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; @@ -28,6 +29,7 @@ public function __construct(Request $request = null) $env = 'test'; } + (new Dotenv())->bootEnv(dirname(__DIR__, 3) . '/.env', $env); $this->kernel = new \AppKernel($env, $debug); $this->kernel->boot(); if (!$request instanceof Request) { diff --git a/sources/AppBundle/Command/UpdateMailchimpMembersCommand.php b/sources/AppBundle/Command/UpdateMailchimpMembersCommand.php index 0ffae73c7..7e90d7208 100644 --- a/sources/AppBundle/Command/UpdateMailchimpMembersCommand.php +++ b/sources/AppBundle/Command/UpdateMailchimpMembersCommand.php @@ -21,7 +21,7 @@ public function __construct( #[Autowire('@app.mailchimp_api')] private Mailchimp $mailchimp, private UserRepository $userRepository, - #[Autowire('%mailchimp_members_list%')] + #[Autowire(env: 'MAILCHIMP_MEMBERS_LIST')] private string $mailchimpMembersList, ) {} diff --git a/sources/AppBundle/Controller/Admin/Event/EventThemeAction.php b/sources/AppBundle/Controller/Admin/Event/EventThemeAction.php new file mode 100644 index 000000000..9b5360c5e --- /dev/null +++ b/sources/AppBundle/Controller/Admin/Event/EventThemeAction.php @@ -0,0 +1,96 @@ +query->get('id'); + + $event = $this->eventActionHelper->getEventById($id); + + // Handle AJAX requests for updating data + if ($request->isXmlHttpRequest()) { + return $this->handleAjaxRequest($request, $event); + } + if ($request->getMethod() === 'POST' && $request->request->has('delete')) { + $theme = $this->eventThemeRepository->get($request->request->getInt('theme_id')); + $this->eventThemeRepository->delete($theme); + $this->addFlash('notice', sprintf('Le thème "%s" a été supprimé.', $theme->getName())); + return $this->redirectToRoute('admin_event_themes_list'); + } + + $themes = $this->eventThemeRepository->getByEventOrderedByPriority($event->getId()); + $scheduledTalks = $this->talkRepository->getScheduledTalksByEvent($event->getId()); + + return $this->render('admin/event/theme_list.html.twig', [ + 'themes' => $themes, + 'scheduled_talks' => $scheduledTalks, + 'event' => $event, + 'event_select_form' => $this->eventSelectFactory->create($event, $request)->createView(), + ]); + } + + private function handleAjaxRequest(Request $request, Event $event): JsonResponse + { + $action = $request->request->get('action'); + + return match ($action) { + 'update_theme_priority' => $this->updateThemePriority($request), + 'update_talk_theme' => $this->updateTalkTheme($request), + default => new JsonResponse(['error' => 'Action non reconnue'], 400), + }; + } + + private function updateThemePriority(Request $request): JsonResponse + { + $themeId = $request->request->getInt('theme_id'); + $priority = $request->request->getInt('priority'); + + $theme = $this->eventThemeRepository->get($themeId); + if (!$theme) { + return new JsonResponse(['error' => 'Thème non trouvé'], 404); + } + + $theme->setPriority($priority); + $this->eventThemeRepository->save($theme); + + return new JsonResponse(['success' => true]); + } + + private function updateTalkTheme(Request $request): JsonResponse + { + $talkId = $request->request->getInt('talk_id'); + $themeId = $request->request->get('theme_id'); + + $talk = $this->talkRepository->get($talkId); + if (!$talk) { + return new JsonResponse(['error' => 'Conférence non trouvée'], 404); + } + + $talk->setTheme($themeId ? (int) $themeId : null); + $this->talkRepository->save($talk); + + return new JsonResponse(['success' => true]); + } +} diff --git a/sources/AppBundle/Controller/Admin/Event/EventThemeAddEditAction.php b/sources/AppBundle/Controller/Admin/Event/EventThemeAddEditAction.php new file mode 100644 index 000000000..6b2f3da6d --- /dev/null +++ b/sources/AppBundle/Controller/Admin/Event/EventThemeAddEditAction.php @@ -0,0 +1,56 @@ +query->has('idForum')) { + $eventTheme->setIdForum($request->query->getInt('idForum')); + } + } else { + $event = $this->eventRepository->get($eventTheme->getIdForum()); + $talks = $this->talkRepository->getByEventWithSpeakers($event, false, false, $eventTheme->getId()); + } + + $form = $this->createForm(EventThemeType::class, $eventTheme); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->eventThemeRepository->save($eventTheme); + + $this->addFlash('notice', 'Thème ' . ($new ? 'ajouté' : 'modifié')); + return $this->redirectToRoute('admin_event_themes_list', ['id' => $eventTheme->getIdForum()]); + } + + return $this->render('admin/event/theme_add_edit.html.twig', [ + 'form' => $form->createView(), + 'eventTheme' => $eventTheme, + 'new' => $new, + 'talks' => $talks ?? null, + ]); + } +} diff --git a/sources/AppBundle/Controller/Admin/TechLetter/GenerateAction.php b/sources/AppBundle/Controller/Admin/TechLetter/GenerateAction.php index 6d33b8c5b..d3cbb37a3 100644 --- a/sources/AppBundle/Controller/Admin/TechLetter/GenerateAction.php +++ b/sources/AppBundle/Controller/Admin/TechLetter/GenerateAction.php @@ -19,7 +19,7 @@ public function __construct( #[Autowire('@app.mailchimp_techletter_api')] private readonly Mailchimp $mailchimp, private readonly TechLetterFactory $techLetterFactory, - #[Autowire('%mailchimp_techletter_list%')] + #[Autowire(env: 'MAILCHIMP_TECHLETTER_LIST')] private readonly string $mailchimpTechletterList, ) {} diff --git a/sources/AppBundle/Controller/Admin/TechLetter/SendTestAction.php b/sources/AppBundle/Controller/Admin/TechLetter/SendTestAction.php index d9214dcd9..e6b7c575b 100644 --- a/sources/AppBundle/Controller/Admin/TechLetter/SendTestAction.php +++ b/sources/AppBundle/Controller/Admin/TechLetter/SendTestAction.php @@ -20,7 +20,7 @@ public function __construct( private readonly SendingRepository $sendingRepository, private readonly Mailer $mailer, private readonly TechLetterFactory $techLetterFactory, - #[Autowire('%techletter_test_email_address%')] + #[Autowire(env: 'TECHLETTER_TEST_EMAIL_ADDRESS')] private readonly string $techletterTestEmailAddress, ) {} diff --git a/sources/AppBundle/Controller/Event/Blog/ProgramAction.php b/sources/AppBundle/Controller/Event/Blog/ProgramAction.php index 16edbc930..1d625773a 100644 --- a/sources/AppBundle/Controller/Event/Blog/ProgramAction.php +++ b/sources/AppBundle/Controller/Event/Blog/ProgramAction.php @@ -6,6 +6,7 @@ use AppBundle\Controller\Event\EventActionHelper; use AppBundle\Event\JsonLd; +use AppBundle\Event\Model\Repository\EventThemeRepository; use AppBundle\Event\Model\Repository\TalkRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -17,13 +18,20 @@ public function __construct( private readonly JsonLd $jsonLd, private readonly EventActionHelper $eventActionHelper, private readonly TalkRepository $talkRepository, + private readonly EventThemeRepository $eventThemeRepository, ) {} public function __invoke(Request $request, $eventSlug): Response { $event = $this->eventActionHelper->getEvent($eventSlug); $jsonld = $this->jsonLd->getDataForEvent($event); - $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event, $request->query->getBoolean('apply-publication-date-filters', true)); + $talkAggregates = $this->talkRepository->getByEventWithSpeakers($event, $request->query->getBoolean('apply-publication-date-filters', true), $event->getHasThemes()); + $themes = null; + if ($event->getHasThemes()) { + $themes = iterator_to_array($this->eventThemeRepository->getBy(['idForum' => $event->getId()])); + usort($themes, fn($a, $b): int => $a->getPriority() === $b->getPriority() ? $a->getName() <=> $b->getName() : $a->getPriority() <=> $b->getPriority()); + $themes = array_combine(array_map(fn($theme): ?int => $theme->getId(), $themes), $themes); + } $now = new \DateTime(); return $this->render( @@ -34,6 +42,7 @@ public function __invoke(Request $request, $eventSlug): Response 'jsonld' => $jsonld, 'speakersPagePrefix' => $request->query->get('speakers-page-prefix', '/' . $event->getPath() . '/speakers/'), 'display_joindin_links' => $now >= $event->getDateStart() && $now <= \DateTimeImmutable::createFromMutable($event->getDateEnd())->modify('+10 days'), + 'themes' => $themes, ], ); } diff --git a/sources/AppBundle/Controller/LegacyController.php b/sources/AppBundle/Controller/LegacyController.php index 69a5b5d08..c5a7b98f8 100644 --- a/sources/AppBundle/Controller/LegacyController.php +++ b/sources/AppBundle/Controller/LegacyController.php @@ -12,6 +12,7 @@ use AppBundle\Event\Invoice\InvoiceService; use AppBundle\Event\Model\Repository\EventRepository; use AppBundle\Event\Model\Repository\EventStatsRepository; +use AppBundle\Event\Model\Repository\EventThemeRepository; use AppBundle\Event\Model\Repository\InvoiceRepository; use AppBundle\Event\Model\Repository\SpeakerRepository; use AppBundle\Event\Model\Repository\TalkRepository; @@ -46,6 +47,7 @@ public function __construct( private readonly SpeakerRepository $speakerRepository, private readonly TalkRepository $talkRepository, private readonly array $backOfficePages, + private readonly EventThemeRepository $eventThemeRepository, ) {} public function void() diff --git a/sources/AppBundle/Controller/Website/Global/HomeAction.php b/sources/AppBundle/Controller/Website/Global/HomeAction.php index 9d56e9b73..072b819cc 100644 --- a/sources/AppBundle/Controller/Website/Global/HomeAction.php +++ b/sources/AppBundle/Controller/Website/Global/HomeAction.php @@ -31,7 +31,7 @@ public function __construct( private readonly CacheItemPoolInterface $cache, private readonly SearchClient $client, private readonly TalkRepository $talkRepository, - #[Autowire('%home_algolia_enabled%')] + #[Autowire(env: 'HOME_ALGOLIA_ENABLED')] private readonly bool $homeAlgoliaEnabled, ) {} diff --git a/sources/AppBundle/Controller/Website/Meetups/ListAction.php b/sources/AppBundle/Controller/Website/Meetups/ListAction.php index f8aebbb1c..fce923971 100644 --- a/sources/AppBundle/Controller/Website/Meetups/ListAction.php +++ b/sources/AppBundle/Controller/Website/Meetups/ListAction.php @@ -14,9 +14,9 @@ final class ListAction extends AbstractController { public function __construct( private readonly ViewRenderer $view, - #[Autowire('%algolia_app_id%')] + #[Autowire(env: 'ALGOLIA_APP_ID')] private readonly string $algoliaAppId, - #[Autowire('%algolia_frontend_api_key%')] + #[Autowire(env: 'ALGOLIA_FRONTEND_API_KEY')] private readonly string $algoliaFrontendApikey, ) {} diff --git a/sources/AppBundle/Controller/Website/NewsletterController.php b/sources/AppBundle/Controller/Website/NewsletterController.php index f5b974c99..7dcd087d5 100644 --- a/sources/AppBundle/Controller/Website/NewsletterController.php +++ b/sources/AppBundle/Controller/Website/NewsletterController.php @@ -19,7 +19,7 @@ public function __construct( private readonly ViewRenderer $view, #[Autowire('@app.mailchimp_api')] private readonly Mailchimp $mailchimp, - #[Autowire('%mailchimp_subscribers_list%')] + #[Autowire(env: 'MAILCHIMP_SUBSCRIBERS_LIST')] private readonly string $mailchimpSubscribersList, ) {} diff --git a/sources/AppBundle/Controller/Website/Static/SuperAperoAction.php b/sources/AppBundle/Controller/Website/Static/SuperAperoAction.php index 0d30b18f1..5466fe131 100644 --- a/sources/AppBundle/Controller/Website/Static/SuperAperoAction.php +++ b/sources/AppBundle/Controller/Website/Static/SuperAperoAction.php @@ -12,7 +12,7 @@ { public function __construct( private ViewRenderer $view, - #[Autowire('%super_apero_csv_url%')] + #[Autowire(env: 'SUPER_APERO_CSV_URL')] private string $superAperoCsvUrl, ) {} diff --git a/sources/AppBundle/Controller/Website/Talks/ListAction.php b/sources/AppBundle/Controller/Website/Talks/ListAction.php index 26c0fab77..868c6d112 100644 --- a/sources/AppBundle/Controller/Website/Talks/ListAction.php +++ b/sources/AppBundle/Controller/Website/Talks/ListAction.php @@ -16,9 +16,9 @@ final class ListAction extends AbstractController { public function __construct( private readonly ViewRenderer $view, - #[Autowire('%algolia_app_id%')] + #[Autowire(env: 'ALGOLIA_APP_ID')] private readonly string $algoliaAppId, - #[Autowire('%algolia_frontend_api_key%')] + #[Autowire(env: 'ALGOLIA_FRONTEND_API_KEY')] private readonly string $algoliaFrontendApikey, ) {} diff --git a/sources/AppBundle/Controller/Website/Techletter/WebhookAction.php b/sources/AppBundle/Controller/Website/Techletter/WebhookAction.php index 9da349ed4..f322ef6ba 100644 --- a/sources/AppBundle/Controller/Website/Techletter/WebhookAction.php +++ b/sources/AppBundle/Controller/Website/Techletter/WebhookAction.php @@ -13,7 +13,7 @@ { public function __construct( private TechletterUnsubscriptionsRepository $techletterUnsubscriptionsRepository, - #[Autowire('%mailchimp_techletter_webhook_key%')] + #[Autowire(env: 'MAILCHIMP_TECHLETTER_WEBHOOK_KEY')] private string $mailchimpTechletterWebhookKey, ) {} diff --git a/sources/AppBundle/Controller/Website/TechnoWatch/CalendarAction.php b/sources/AppBundle/Controller/Website/TechnoWatch/CalendarAction.php index 2b39955a6..08bf45005 100644 --- a/sources/AppBundle/Controller/Website/TechnoWatch/CalendarAction.php +++ b/sources/AppBundle/Controller/Website/TechnoWatch/CalendarAction.php @@ -13,9 +13,9 @@ final class CalendarAction extends AbstractController { public function __construct( - #[Autowire('%techno_watch_calendar_key%')] + #[Autowire(env: 'TECHNO_WATCH_CALENDAR_KEY')] private readonly string $technoWatchCalendarKey, - #[Autowire('%techno_watch_calendar_url%')] + #[Autowire(env: 'TECHNO_WATCH_CALENDAR_URL')] private readonly string $technoWatchCalendarUrl, ) {} diff --git a/sources/AppBundle/Event/Form/EventThemeType.php b/sources/AppBundle/Event/Form/EventThemeType.php new file mode 100644 index 000000000..69a8c9a33 --- /dev/null +++ b/sources/AppBundle/Event/Form/EventThemeType.php @@ -0,0 +1,64 @@ +eventHelper = new EventHelper(); + } + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $idForumField = $builder->create('idForum', ChoiceType::class, [ + 'label' => 'Évènement', + 'choice_label' => 'title', + 'choice_value' => fn(?Event $event): ?string => $event?->getId() !== null ? (string) $event->getId() : null, + 'choices' => $this->eventHelper->sortEventsByStartDate( + iterator_to_array($this->eventRepository->getAllActive()), + ), + 'group_by' => fn(Event $choice): string => $this->eventHelper->groupByYear($choice), + ]); + + $idForumField->addModelTransformer(new CallbackTransformer( + fn(?int $idForum): ?Event => $idForum ? $this->eventRepository->getOneBy(['id' => $idForum]) : null, + fn(?Event $event): ?int => $event?->getId(), + )); + $builder->add($idForumField) + ->add('name', TextType::class, [ + 'label' => 'Nom du thème', + 'constraints' => [new Assert\NotBlank(['message' => 'Titre du forum manquant'])], + ]) + ->add('description', TextareaType::class, [ + 'label' => 'Description', + 'help' => 'Le thème de description apparait sur la page de programme', + 'constraints' => [new Assert\NotBlank(), new Assert\Length(['max' => 600])], + 'attr' => ['class' => 'simplemde'], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => EventTheme::class, + ]); + } +} diff --git a/sources/AppBundle/Event/Form/EventType.php b/sources/AppBundle/Event/Form/EventType.php index 46877969c..996b0c662 100644 --- a/sources/AppBundle/Event/Form/EventType.php +++ b/sources/AppBundle/Event/Form/EventType.php @@ -114,6 +114,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'Date annonce planning', 'required' => false, ]) + ->add('hasThemes', CheckboxType::class, [ + 'label' => 'Activer le support des thèmes', + 'required' => false, + ]) ->add('dateEndSalesSponsorToken', DateTimeType::class, [ 'widget' => 'single_text', 'label' => 'Date fin saisie token sponsor', diff --git a/sources/AppBundle/Event/Model/Event.php b/sources/AppBundle/Event/Model/Event.php index e68f93b25..136208b05 100644 --- a/sources/AppBundle/Event/Model/Event.php +++ b/sources/AppBundle/Event/Model/Event.php @@ -98,6 +98,8 @@ class Event implements NotifyPropertyInterface private ?DateTime $archivedAt = null; + private ?bool $hasThemes = false; + /** * @return int */ @@ -709,6 +711,19 @@ public function isOnline(): bool return str_contains($this->getPath(), 'enligne'); } + public function getHasThemes(): bool + { + return $this->hasThemes; + } + + public function setHasThemes(bool $hasThemes): self + { + $this->propertyChanged('hasThemes', $this->hasThemes, $hasThemes); + $this->hasThemes = $hasThemes; + + return $this; + } + public static function getInscriptionAttachmentDir(): string { return __DIR__ . '/../../../../htdocs/uploads/mail_inscription_attachment/'; diff --git a/sources/AppBundle/Event/Model/EventTheme.php b/sources/AppBundle/Event/Model/EventTheme.php new file mode 100644 index 000000000..264854436 --- /dev/null +++ b/sources/AppBundle/Event/Model/EventTheme.php @@ -0,0 +1,86 @@ +id; + } + + public function setId(?int $id): void + { + $this->propertyChanged('id', $this->id ?? null, $id); + $this->id = $id; + } + + public function getIdForum(): ?int + { + return $this->idForum; + } + + public function setIdForum(?int $idForum): void + { + $this->propertyChanged('idForum', $this->idForum ?? null, $idForum); + $this->idForum = $idForum; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->propertyChanged('name', $this->name ?? '', $name); + $this->name = $name; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->propertyChanged('description', $this->description ?? '', $description); + $this->description = $description; + } + + public function getPriority(): int + { + return $this->priority; + } + + public function setPriority(int $priority): void + { + $this->propertyChanged('priority', $this->priority ?? 0, $priority); + $this->priority = $priority; + } +} diff --git a/sources/AppBundle/Event/Model/Repository/EventRepository.php b/sources/AppBundle/Event/Model/Repository/EventRepository.php index 74c479dab..e745f87c3 100644 --- a/sources/AppBundle/Event/Model/Repository/EventRepository.php +++ b/sources/AppBundle/Event/Model/Repository/EventRepository.php @@ -8,10 +8,12 @@ use AppBundle\Event\Model\GithubUser; use AppBundle\Event\Model\Ticket; use AppBundle\Ting\DateTimeWithTimeZoneSerializer; +use Aura\SqlQuery\Mysql\Select; use CCMBenchmark\Ting\Driver\Mysqli\Serializer\Boolean; use CCMBenchmark\Ting\Exception; use CCMBenchmark\Ting\Query\QueryException; use CCMBenchmark\Ting\Repository\CollectionInterface; +use CCMBenchmark\Ting\Repository\Hydrator; use CCMBenchmark\Ting\Repository\HydratorArray; use CCMBenchmark\Ting\Repository\HydratorSingleObject; use CCMBenchmark\Ting\Repository\Metadata; @@ -260,6 +262,24 @@ public function getAllActive(): CollectionInterface return $query->query($this->getCollection(new HydratorSingleObject())); } + public function getBySlugWithThemes(string $slug): ?Event + { + /** + * @var Select $queryBuilder + */ + $queryBuilder = $this->getQueryBuilder(Select::class); + $queryBuilder + ->cols(['*']) + ->from('afup_forum') + ->leftJoin('afup_conference_theme', 'afup_conference_themes.id_forum', 'afup_forum.id') + ->where('path = :path') + ->orderBy('afup_conference_theme.name') + ; + $query = $this->getPreparedQuery($queryBuilder->getStatement()); + $query->setParams(['path' => $slug]); + return $query->query($this->getCollection(new Hydrator()))->first(); + } + public static function initMetadata(SerializerFactoryInterface $serializerFactory, array $options = []) { $metadata = new Metadata($serializerFactory); @@ -267,6 +287,7 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor $metadata->setConnectionName('main'); $metadata->setDatabase($options['database']); $metadata->setTable('afup_forum'); + $metadata->setRepository(self::class); $metadata ->addField([ @@ -466,6 +487,12 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor 'fieldName' => 'archivedAt', 'type' => 'datetime', ]) + ->addField([ + 'columnName' => 'has_themes', + 'fieldName' => 'hasThemes', + 'type' => 'bool', + 'serializer' => Boolean::class, + ]) ; return $metadata; diff --git a/sources/AppBundle/Event/Model/Repository/EventThemeRepository.php b/sources/AppBundle/Event/Model/Repository/EventThemeRepository.php new file mode 100644 index 000000000..955de1172 --- /dev/null +++ b/sources/AppBundle/Event/Model/Repository/EventThemeRepository.php @@ -0,0 +1,23 @@ + + */ +class EventThemeRepository extends Repository +{ + /** + * @return CollectionInterface + */ + public function getByEventOrderedByPriority(int $eventId): CollectionInterface + { + return $this->getBy(['idForum' => $eventId], ['priority' => 'ASC', 'name' => 'ASC']); + } +} diff --git a/sources/AppBundle/Event/Model/Repository/TalkRepository.php b/sources/AppBundle/Event/Model/Repository/TalkRepository.php index 0678a4e2b..cd33a09f5 100644 --- a/sources/AppBundle/Event/Model/Repository/TalkRepository.php +++ b/sources/AppBundle/Event/Model/Repository/TalkRepository.php @@ -225,33 +225,40 @@ public function getByTalkWithSpeakers(Talk $talk) /** * @param bool $applyPublicationdateFilters + * @param bool $orderByTheme * * @return array * @throws QueryException */ - public function getByEventWithSpeakers(Event $event, $applyPublicationdateFilters = true): array + public function getByEventWithSpeakers(Event $event, bool $applyPublicationdateFilters = true, bool $orderByTheme = false, ?int $filterByThemeId = null): array { - return $this->getByEventsWithSpeakers([$event], $applyPublicationdateFilters); + return $this->getByEventsWithSpeakers([$event], $applyPublicationdateFilters, $orderByTheme, $filterByThemeId); } /** * @param list $events * @param bool $applyPublicationdateFilters + * @param bool $orderByTheme * * @return array * @throws QueryException */ - public function getByEventsWithSpeakers(array $events, $applyPublicationdateFilters = true): array + public function getByEventsWithSpeakers(array $events, bool $applyPublicationdateFilters = true, bool $orderByTheme = false, ?int $filterByThemeId = null): array { $hydrator = new JoinHydrator(); $hydrator->aggregateOn('talk', 'speaker', 'getId'); + + $params = []; $publicationdateFilters = ''; if ($applyPublicationdateFilters) { $publicationdateFilters = 'AND (talk.date_publication < NOW() OR talk.date_publication IS NULL)'; } - - $params = []; + $themeFilters = ''; + if ($filterByThemeId) { + $themeFilters = 'AND afup_conference_theme.id = :filterByThemeId'; + $params['filterByThemeId'] = $filterByThemeId; + } $inEventsKeys = []; $cpt = 0; @@ -266,7 +273,7 @@ public function getByEventsWithSpeakers(array $events, $applyPublicationdateFilt $query = $this->getPreparedQuery( sprintf('SELECT talk.id_forum, talk.session_id, titre, skill, genre, abstract, talk.plannifie, talk.language_code, - talk.joindin, + talk.joindin, talk.theme, speaker.conferencier_id, speaker.nom, speaker.prenom, speaker.id_forum, speaker.photo, speaker.societe, planning.debut, planning.fin, room.id, room.nom FROM afup_sessions AS talk @@ -274,8 +281,9 @@ public function getByEventsWithSpeakers(array $events, $applyPublicationdateFilt LEFT JOIN afup_conferenciers speaker ON speaker.conferencier_id = acs.conferencier_id LEFT JOIN afup_forum_planning planning ON planning.id_session = talk.session_id LEFT JOIN afup_forum_salle room ON planning.id_salle = room.id - WHERE talk.id_forum IN(%s) AND plannifie = 1 %s - ORDER BY planning.debut ASC, room.id ASC, talk.date_publication DESC, talk.session_id ASC ', $inEvents, $publicationdateFilters), + LEFT JOIN afup_conference_theme ON afup_conference_theme.id = talk.theme + WHERE talk.id_forum IN(%s) AND plannifie = 1 %s %s + ORDER BY %s ', $inEvents, $publicationdateFilters, $themeFilters, $orderByTheme ? 'afup_conference_theme.priority ASC, afup_conference_theme.name ASC' : 'planning.debut ASC, room.id ASC, talk.date_publication DESC, talk.session_id ASC '), )->setParams($params); $result = $query->query($this->getCollection($hydrator)); @@ -360,6 +368,20 @@ public function findList(array $talkIds): CollectionInterface ->query($this->getCollection(new HydratorSingleObject())); } + /** + * @return CollectionInterface + */ + public function getScheduledTalksByEvent(int $eventId): CollectionInterface + { + $query = $this->getPreparedQuery( + 'SELECT * FROM afup_sessions + WHERE id_forum = :eventId AND plannifie = 1 + ORDER BY titre ASC', + )->setParams(['eventId' => $eventId]); + + return $query->query($this->getCollection(new HydratorSingleObject())); + } + /** * @return Metadata */ @@ -513,7 +535,13 @@ public static function initMetadata(SerializerFactoryInterface $serializerFactor 'fieldName' => 'hasAllowedToSharingWithLocalOffices', 'type' => 'bool', 'serializer' => Boolean::class, - ]); + ]) + ->addField([ + 'columnName' => 'theme', + 'fieldName' => 'theme', + 'type' => 'int', + ]) + ; return $metadata; } diff --git a/sources/AppBundle/Event/Model/Talk.php b/sources/AppBundle/Event/Model/Talk.php index f0eb08676..fe21e9194 100644 --- a/sources/AppBundle/Event/Model/Talk.php +++ b/sources/AppBundle/Event/Model/Talk.php @@ -96,6 +96,8 @@ class Talk implements NotifyPropertyInterface */ private array $votes = []; + private ?int $theme = null; + public function getId(): ?int { return $this->id; @@ -605,4 +607,15 @@ public function setVotes(array $votes): self $this->votes = $votes; return $this; } + + public function getTheme(): ?int + { + return $this->theme; + } + + public function setTheme(?int $theme): void + { + $this->propertyChanged('theme', $this->theme, $theme); + $this->theme = $theme; + } } diff --git a/sources/AppBundle/Mailchimp/Runner.php b/sources/AppBundle/Mailchimp/Runner.php index 047793efa..ca7aa8356 100644 --- a/sources/AppBundle/Mailchimp/Runner.php +++ b/sources/AppBundle/Mailchimp/Runner.php @@ -14,7 +14,7 @@ public function __construct( #[Autowire('@app.mailchimp_api')] private Mailchimp $mailchimp, private UserRepository $userRepository, - #[Autowire('%mailchimp_members_list%')] + #[Autowire(env: 'MAILCHIMP_MEMBERS_LIST')] private string $membersListId, ) {} diff --git a/sources/AppBundle/Notifier/SlackNotifier.php b/sources/AppBundle/Notifier/SlackNotifier.php index 4b7ae98a6..838495285 100644 --- a/sources/AppBundle/Notifier/SlackNotifier.php +++ b/sources/AppBundle/Notifier/SlackNotifier.php @@ -16,7 +16,7 @@ final readonly class SlackNotifier { public function __construct( - #[Autowire('%slack_url%')] + #[Autowire(env: 'SLACK_URL')] private string $postUrl, private MessageFactory $messageFactory, #[Autowire('@jms_serializer.serializer')] diff --git a/sources/AppBundle/Slack/UsersClient.php b/sources/AppBundle/Slack/UsersClient.php index 7b8014352..e9f42453a 100644 --- a/sources/AppBundle/Slack/UsersClient.php +++ b/sources/AppBundle/Slack/UsersClient.php @@ -12,9 +12,9 @@ public const USER_LIST_API = '/users.list'; public function __construct( - #[Autowire('%slack_membre_token%')] + #[Autowire(env: 'SLACK_MEMBRE_TOKEN')] private string $token, - #[Autowire('%slack_api_url%')] + #[Autowire(env: 'SLACK_API_URL')] private string $apiBaseUrl, ) {} diff --git a/sources/AppBundle/Twig/TwigExtension.php b/sources/AppBundle/Twig/TwigExtension.php index d6253f9ea..75086e281 100644 --- a/sources/AppBundle/Twig/TwigExtension.php +++ b/sources/AppBundle/Twig/TwigExtension.php @@ -16,9 +16,9 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface public function __construct( private readonly Parsedown $parsedown, private readonly Parsedown $emailParsedown, - #[Autowire('%google_analytics_enabled%')] + #[Autowire(env: 'GOOGLE_ANALYTICS_ENABLED')] private readonly string $googleAnalyticsEnabled, - #[Autowire('%google_analytics_id%')] + #[Autowire(env: 'GOOGLE_ANALYTICS_ID')] private readonly string $googleAnalyticsId, ) {} diff --git a/templates/admin/event/form.html.twig b/templates/admin/event/form.html.twig index d168b87c8..f4b6963fe 100644 --- a/templates/admin/event/form.html.twig +++ b/templates/admin/event/form.html.twig @@ -63,6 +63,7 @@ {{ form_row(form.dateEndVote) }}
{{ form_row(form.datePlanningAnnouncement) }} + {{ form_row(form.hasThemes) }} diff --git a/templates/admin/event/theme_add_edit.html.twig b/templates/admin/event/theme_add_edit.html.twig new file mode 100644 index 000000000..1b11770a2 --- /dev/null +++ b/templates/admin/event/theme_add_edit.html.twig @@ -0,0 +1,97 @@ +{% form_theme form 'form_theme_admin.html.twig' %} + +{% extends 'admin/base_with_header.html.twig' %} + +{% block content %} +

{% if new %}Ajouter{% else %}Modifier{% endif%} un thème

+ + + {{ form_start(form, {'attr': {novalidate: true}}) }} + {{ form_errors(form) }} +
+

Thème

+
+ +
+ {{ form_row(form.idForum) }} + {{ form_row(form.name) }} + + +
+ {{ form_label(form.description) }} +
+ {{ form_widget(form.description) }} + {{ form_errors(form.description) }} +
+
+
+
+ +
+ +
+
+
+ +
+
+ + {% if talks|length > 0 %} +
+
+ Suggestion de prompt pour générer la description + + +
+## Prompt
+Tu es un assistant expert en webmarketing chargé de rédiger une courte description promotionnelle pour une thématique de talks lors du prochain événement de l'AFUP.
+En entrée, tu recevras une liste de talks portant sur une même thématique ainsi que des informations sur les speakers.
+Ton objectif : rédiger un texte accrocheur et fluide qui donne envie d'assister aux talks, tout en mettant en valeur les enjeux et les mots-clés de la thématique (SEO-friendly, sans excès).
+Contraintes :
+– Texte dynamique, sans répétitions, ni énumérations
+– Majuscules uniquement là où elles sont grammaticalement nécessaires
+– Pas de ponctuation ou de mise en forme inutile (pas de tirets quadratins ni d’émojis, sauf si justifiés)
+– 600 caractères maximum
+
+Ne fournis que la description, sans commentaire ni mise en contexte.
+
+## Thématique
+{{ eventTheme.name }}
+
+## Talks
+{% for talk in talks %}
+### Titre: {{ talk.talk.title }}
+{% for speaker in talk.speakers %}
+{% if speaker is not null %}
+### Speaker: {{ speaker.label }}
+{% endif ~%}
+{% endfor ~%}
+### Description du talk
+{{ talk.talk.description }}
+
+{% endfor ~%}
+            
+ +
+
+ {% endif %} + +
+

* indique un champ obligatoire

+
+ {{ form_end(form) }} + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/event/theme_list.html.twig b/templates/admin/event/theme_list.html.twig new file mode 100644 index 000000000..6187a11d5 --- /dev/null +++ b/templates/admin/event/theme_list.html.twig @@ -0,0 +1,229 @@ +{% extends 'admin/base_with_header.html.twig' %} + +{% block content %} + {% set token_delete=csrf_token('forum_delete') %} +

Liste des thèmes

+ + {% include 'admin/event/change_event.html.twig' with {form: event_select_form} only %} + + +

Gestion des thèmes et de leurs positions

+ + + + + + + + + + {% for theme in themes %} + + + + + + {% else %} + + + + {% endfor %} + +
NomPositionActions
{{ theme.name }} + + + + + +
+ + +
+
+
+ + Aucun thème. {% if event == null %}Essayez de changer d'évènement !{% endif %} +
+
+ +

Association des conférences aux thèmes

+
+ {% if scheduled_talks %} + + + + + + + + + + {% for talk in scheduled_talks %} + + + + + + {% endfor %} + +
ConférenceRésuméThème
+ {{ talk.title }} + + {% set abstract = talk.abstract %} + {% if abstract|length > 400 %} +
+ {{ abstract|slice(0, 400) }}... + +
+ Voir plus +
+ {% else %} +
+ {{ abstract|raw|markdown }} +
+ {% endif %} +
+
+ +
+
+ {% else %} +
+ + Aucune conférence planifiée pour cet événement. +
+ {% endif %} +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/blog/program.html.twig b/templates/blog/program.html.twig index be3c86744..a79c918d3 100644 --- a/templates/blog/program.html.twig +++ b/templates/blog/program.html.twig @@ -2,9 +2,30 @@ {{ jsonld|json_encode|raw }} +{% macro theme_header(currentTheme, themes) %} + {% if themes is not null and themes[currentTheme] is defined %} +
+ +
+

{{ themes[currentTheme].description }}

+ {% endif %} +{% endmacro %} + {% if talks|length %}
+ {% set last_theme = null %} {% for talk in talks %} + {% if themes is not null and talk.talk.theme != last_theme %} + {% set last_theme = talk.talk.theme %} + {{ _self.theme_header(last_theme, themes) }} + {% endif %}

{{ talk.talk.title|raw }}