From 3cea86b4d3db332e4766d723b118df9ede538e6e Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:07:30 +0200 Subject: [PATCH 1/7] Provide new CSS components --- assets/stylesheets/application.css | 4 +++ assets/stylesheets/components/anchors.css | 25 ++++++++++++++++++ assets/stylesheets/components/cards.css | 18 +++++++++++++ assets/stylesheets/components/grid.css | 31 +++++++++++++++++++++++ assets/stylesheets/components/text.css | 15 +++++++++++ 5 files changed, 93 insertions(+) create mode 100644 assets/stylesheets/components/anchors.css create mode 100644 assets/stylesheets/components/cards.css create mode 100644 assets/stylesheets/components/grid.css create mode 100644 assets/stylesheets/components/text.css diff --git a/assets/stylesheets/application.css b/assets/stylesheets/application.css index b14bb07d..bb0710a6 100644 --- a/assets/stylesheets/application.css +++ b/assets/stylesheets/application.css @@ -7,9 +7,13 @@ @import "./utils/flow.css"; @import "./utils/wrapper.css"; @import "./components/alerts.css"; +@import "./components/anchors.css"; @import "./components/buttons.css"; +@import "./components/cards.css"; @import "./components/forms.css"; +@import "./components/grid.css"; @import "./components/layout.css"; +@import "./components/text.css"; *, *::before, diff --git a/assets/stylesheets/components/anchors.css b/assets/stylesheets/components/anchors.css new file mode 100644 index 00000000..c750cef2 --- /dev/null +++ b/assets/stylesheets/components/anchors.css @@ -0,0 +1,25 @@ +/* This file is part of Bileto. */ +/* Copyright 2022 Probesys */ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ + +a { + color: var(--color-primary11); +} + +.anchor--action { + display: inline-block; + padding: 0.5rem 1.75rem; + + text-decoration: none; + + background-color: var(--color-primary3); + border: 0.25rem solid currentcolor; + border-radius: 0.5rem; + + transition: background-color 0.2s ease-in-out; +} + +.anchor--action:hover, +.anchor--action:focus { + background-color: var(--color-primary4); +} diff --git a/assets/stylesheets/components/cards.css b/assets/stylesheets/components/cards.css new file mode 100644 index 00000000..0a17d621 --- /dev/null +++ b/assets/stylesheets/components/cards.css @@ -0,0 +1,18 @@ +/* This file is part of Bileto. */ +/* Copyright 2022 Probesys */ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ + +.card { + overflow: hidden; + + padding: 1rem; + + background-color: var(--color-grey4); + box-shadow: 0 1px 2px var(--color-grey8); + border-radius: 0.5rem; +} + +.card__title { + font-weight: bold; + overflow-wrap: anywhere; +} diff --git a/assets/stylesheets/components/grid.css b/assets/stylesheets/components/grid.css new file mode 100644 index 00000000..28478174 --- /dev/null +++ b/assets/stylesheets/components/grid.css @@ -0,0 +1,31 @@ +/* This file is part of Bileto. */ +/* Copyright 2022 Probesys */ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ + +.grid { + display: grid; + + grid-gap: 2rem; +} + +.grid > * { + min-width: 0; +} + +@media (min-width: 800px) { + .grid--cols2 { + grid-template-columns: repeat(2, 1fr); + } + + .grid--cols3 { + grid-template-columns: repeat(3, 1fr); + } + + .grid--cols4 { + grid-template-columns: repeat(4, 1fr); + } + + .grid--cols5 { + grid-template-columns: repeat(5, 1fr); + } +} diff --git a/assets/stylesheets/components/text.css b/assets/stylesheets/components/text.css new file mode 100644 index 00000000..f6da8769 --- /dev/null +++ b/assets/stylesheets/components/text.css @@ -0,0 +1,15 @@ +/* This file is part of Bileto. */ +/* Copyright 2022 Probesys */ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ + +.text--secondary { + color: var(--color-grey11); +} + +.text--center { + text-align: center; +} + +.text--big { + font-size: 1.2em; +} From f80b8c19314ddef99e2e992297bef960e5f1fab4 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:07:44 +0200 Subject: [PATCH 2/7] Improve support for forms errors --- assets/stylesheets/components/forms.css | 18 +++++++++ config/packages/twig.yaml | 3 ++ src/Controller/BaseController.php | 54 +++++++++++++++++++++++++ src/Controller/HomeController.php | 3 +- src/Controller/LoginController.php | 3 +- 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/Controller/BaseController.php diff --git a/assets/stylesheets/components/forms.css b/assets/stylesheets/components/forms.css index b8bfaf09..15c1d379 100644 --- a/assets/stylesheets/components/forms.css +++ b/assets/stylesheets/components/forms.css @@ -27,6 +27,24 @@ input:focus { border-color: var(--color-primary8); } +input[aria-invalid] { + background-color: var(--color-error2); + border-color: var(--color-error11); +} + +.form__error { + padding-left: 1.25em; + + color: var(--color-error11); + font-size: 0.9em; + font-weight: bold; + + background-image: url("../../icons/circle-exclamation-error.svg"); + background-repeat: no-repeat; + background-position: left center; + background-size: 1em; +} + .form__actions { display: flex; diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 030a40e5..6922e5ee 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -4,6 +4,9 @@ twig: default_path: '%kernel.project_dir%/templates' + globals: + errors: [] + error: null when@test: twig: diff --git a/src/Controller/BaseController.php b/src/Controller/BaseController.php new file mode 100644 index 00000000..d31f686d --- /dev/null +++ b/src/Controller/BaseController.php @@ -0,0 +1,54 @@ +setStatusCode(Response::HTTP_BAD_REQUEST); + } + + return $this->render($view, $parameters, $response); + } + + protected function csrfError(): string + { + return new TranslatableMessage('Invalid CSRF token.', [], 'security'); + } + + /** + * @return array + */ + protected function formatErrors(ConstraintViolationListInterface $errors): array + { + $formattedErrors = []; + foreach ($errors as $error) { + $property = $error->getPropertyPath(); + if (isset($formattedErrors[$property])) { + $formattedErrors[$property] = implode( + ' ', + [$formattedErrors[$property], $error->getMessage()], + ); + } else { + $formattedErrors[$property] = $error->getMessage(); + } + } + return $formattedErrors; + } +} diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 0f9981e6..f1e38380 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -8,9 +8,8 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -class HomeController extends AbstractController +class HomeController extends BaseController { #[Route('/', name: 'home', methods: ['GET', 'HEAD'])] public function show(): Response diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 5404182b..91c6ed4e 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -6,12 +6,11 @@ namespace App\Controller; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; -class LoginController extends AbstractController +class LoginController extends BaseController { #[Route('/login', name: 'login', methods: ['GET', 'POST'])] public function new(AuthenticationUtils $authenticationUtils): Response From ff2a681c734106a8915b95b12e815fcc80cf6f27 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:08:46 +0200 Subject: [PATCH 3/7] Add support for layout "back" navigation --- assets/stylesheets/components/layout.css | 18 ++++++++++++++++++ templates/base.html.twig | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/assets/stylesheets/components/layout.css b/assets/stylesheets/components/layout.css index 8d6feb6a..f7c8bf20 100644 --- a/assets/stylesheets/components/layout.css +++ b/assets/stylesheets/components/layout.css @@ -34,6 +34,24 @@ flex: 1; } +.layout__back { + max-width: 1200px; + margin-top: -1rem; + margin-right: auto; + margin-left: auto; +} + +.layout__back a { + display: inline-block; + padding: 2rem 1.5rem; + + outline-offset: -0.3rem; +} + +.layout__back a::before { + content: "←"; +} + .layout__body { padding: 2rem 1rem; diff --git a/templates/base.html.twig b/templates/base.html.twig index c9c8a9fb..2cfa1f0c 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -44,6 +44,12 @@ + {% if block("back") is defined %} +
+ {{ block("back") }} +
+ {% endif %} + {% block body %}{% endblock %} From d69de39d0d4eb895fc9134cdfe388bf0a76db384 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:09:46 +0200 Subject: [PATCH 4/7] Improve layout spacing --- assets/stylesheets/components/layout.css | 10 +++++++++- assets/stylesheets/utils/flow.css | 6 +++--- templates/login/new.html.twig | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/assets/stylesheets/components/layout.css b/assets/stylesheets/components/layout.css index f7c8bf20..ed59d7dd 100644 --- a/assets/stylesheets/components/layout.css +++ b/assets/stylesheets/components/layout.css @@ -53,17 +53,25 @@ } .layout__body { - padding: 2rem 1rem; + padding: 2rem 1rem 4rem; background-color: var(--color-grey1); box-shadow: 0 1px 3px var(--color-grey7); } +.layout__body--small { + max-width: 600px; + margin-right: auto; + margin-left: auto; +} + @media (min-width: 800px) { .layout__body { padding-right: 2rem; padding-left: 2rem; + } + .layout__body--small { border-radius: 0.5rem; } } diff --git a/assets/stylesheets/utils/flow.css b/assets/stylesheets/utils/flow.css index 4d04463c..0ee125e4 100644 --- a/assets/stylesheets/utils/flow.css +++ b/assets/stylesheets/utils/flow.css @@ -11,13 +11,13 @@ } .flow > * + * { - margin-top: 1.5rem; + margin-top: 2rem; } .flow-large > * + * { - margin-top: 1.75rem; + margin-top: 3rem; } .flow-larger > * + * { - margin-top: 2.5rem; + margin-top: 4rem; } diff --git a/templates/login/new.html.twig b/templates/login/new.html.twig index 0827e2d6..21d5dfa3 100644 --- a/templates/login/new.html.twig +++ b/templates/login/new.html.twig @@ -9,7 +9,7 @@ {% block title %}{{ 'Log in' | trans }}{% endblock %} {% block body %} -
+

{{ 'Log in' | trans }}

From f0521457f4033272a9f22a1ef05301fe930e303b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:10:47 +0200 Subject: [PATCH 5/7] Add an Organization entity --- ...ersion20221014081029CreateOrganization.php | 59 ++++++++++++++++ src/Entity/Organization.php | 53 ++++++++++++++ src/Factory/OrganizationFactory.php | 70 +++++++++++++++++++ src/Repository/OrganizationRepository.php | 45 ++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 migrations/Version20221014081029CreateOrganization.php create mode 100644 src/Entity/Organization.php create mode 100644 src/Factory/OrganizationFactory.php create mode 100644 src/Repository/OrganizationRepository.php diff --git a/migrations/Version20221014081029CreateOrganization.php b/migrations/Version20221014081029CreateOrganization.php new file mode 100644 index 00000000..9076ef0a --- /dev/null +++ b/migrations/Version20221014081029CreateOrganization.php @@ -0,0 +1,59 @@ +connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('CREATE SEQUENCE organization_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql(<<addSql('CREATE UNIQUE INDEX UNIQ_C1EE637C5E237E06 ON "organization" (name)'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql(<<addSql('CREATE UNIQUE INDEX UNIQ_C1EE637C5E237E06 ON organization (name)'); + } + } + + public function down(Schema $schema): void + { + $dbPlatform = $this->connection->getDatabasePlatform()->getName(); + if ($dbPlatform === 'postgresql') { + $this->addSql('DROP INDEX UNIQ_C1EE637C5E237E06'); + $this->addSql('DROP SEQUENCE organization_id_seq CASCADE'); + $this->addSql('DROP TABLE organization'); + } elseif ($dbPlatform === 'mysql') { + $this->addSql('DROP INDEX UNIQ_C1EE637C5E237E06 on `organization`'); + $this->addSql('DROP TABLE organization'); + } + } +} diff --git a/src/Entity/Organization.php b/src/Entity/Organization.php new file mode 100644 index 00000000..ceebaf2b --- /dev/null +++ b/src/Entity/Organization.php @@ -0,0 +1,53 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } +} diff --git a/src/Factory/OrganizationFactory.php b/src/Factory/OrganizationFactory.php new file mode 100644 index 00000000..febf1adf --- /dev/null +++ b/src/Factory/OrganizationFactory.php @@ -0,0 +1,70 @@ + + * + * @method static Organization|Proxy createOne(array $attributes = []) + * @method static Organization[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Organization[]|Proxy[] createSequence(array|callable $sequence) + * @method static Organization|Proxy find(object|array|mixed $criteria) + * @method static Organization|Proxy findOrCreate(array $attributes) + * @method static Organization|Proxy first(string $sortedField = 'id') + * @method static Organization|Proxy last(string $sortedField = 'id') + * @method static Organization|Proxy random(array $attributes = []) + * @method static Organization|Proxy randomOrCreate(array $attributes = []) + * @method static Organization[]|Proxy[] all() + * @method static Organization[]|Proxy[] findBy(array $attributes) + * @method static Organization[]|Proxy[] randomSet(int $number, array $attributes = []) + * @method static Organization[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static OrganizationRepository|RepositoryProxy repository() + * @method Organization|Proxy create(array|callable $attributes = []) + * + * @phpstan-method static Organization&Proxy createOne(array $attributes = []) + * @phpstan-method static Organization[]&Proxy[] createMany(int $number, array|callable $attributes = []) + * @phpstan-method static Organization[]&Proxy[] createSequence(array|callable $sequence) + * @phpstan-method static Organization&Proxy find(object|array|mixed $criteria) + * @phpstan-method static Organization&Proxy findOrCreate(array $attributes) + * @phpstan-method static Organization&Proxy first(string $sortedField = 'id') + * @phpstan-method static Organization&Proxy last(string $sortedField = 'id') + * @phpstan-method static Organization&Proxy random(array $attributes = []) + * @phpstan-method static Organization&Proxy randomOrCreate(array $attributes = []) + * @phpstan-method static Organization[]&Proxy[] all() + * @phpstan-method static Organization[]&Proxy[] findBy(array $attributes) + * @phpstan-method static Organization[]&Proxy[] randomSet(int $number, array $attributes = []) + * @phpstan-method static Organization[]&Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @phpstan-method Organization&Proxy create(array|callable $attributes = []) + */ +final class OrganizationFactory extends ModelFactory +{ + /** + * @return mixed[] + */ + protected function getDefaults(): array + { + return [ + 'name' => self::faker()->text(), + ]; + } + + protected function initialize(): self + { + return $this; + } + + protected static function getClass(): string + { + return Organization::class; + } +} diff --git a/src/Repository/OrganizationRepository.php b/src/Repository/OrganizationRepository.php new file mode 100644 index 00000000..655dac60 --- /dev/null +++ b/src/Repository/OrganizationRepository.php @@ -0,0 +1,45 @@ + + * + * @method Organization|null find($id, $lockMode = null, $lockVersion = null) + * @method Organization|null findOneBy(array $criteria, array $orderBy = null) + * @method Organization[] findAll() + * @method Organization[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class OrganizationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Organization::class); + } + + public function save(Organization $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Organization $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } +} From d4f7c4b87067e20550526a5426975fe208019fc5 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:12:46 +0200 Subject: [PATCH 6/7] Allow to list and create organizations --- src/Controller/HomeController.php | 2 +- src/Controller/OrganizationsController.php | 68 +++++++ templates/home/show.html.twig | 13 -- templates/organizations/index.html.twig | 45 +++++ templates/organizations/new.html.twig | 71 ++++++++ tests/Controller/HomeControllerTest.php | 5 +- tests/Controller/LoginControllerTest.php | 2 +- .../OrganizationsControllerTest.php | 170 ++++++++++++++++++ 8 files changed, 358 insertions(+), 18 deletions(-) create mode 100644 src/Controller/OrganizationsController.php delete mode 100644 templates/home/show.html.twig create mode 100644 templates/organizations/index.html.twig create mode 100644 templates/organizations/new.html.twig create mode 100644 tests/Controller/OrganizationsControllerTest.php diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index f1e38380..14587a90 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -14,6 +14,6 @@ class HomeController extends BaseController #[Route('/', name: 'home', methods: ['GET', 'HEAD'])] public function show(): Response { - return $this->render('home/show.html.twig'); + return $this->redirectToRoute('organizations'); } } diff --git a/src/Controller/OrganizationsController.php b/src/Controller/OrganizationsController.php new file mode 100644 index 00000000..eee08eea --- /dev/null +++ b/src/Controller/OrganizationsController.php @@ -0,0 +1,68 @@ +findBy([], ['name' => 'ASC']); + return $this->render('organizations/index.html.twig', [ + 'organizations' => $organizations, + ]); + } + + #[Route('/organizations/new', name: 'new organization', methods: ['GET', 'HEAD'])] + public function new(): Response + { + return $this->render('organizations/new.html.twig', [ + 'name' => '', + ]); + } + + #[Route('/organizations/new', name: 'create organization', methods: ['POST'])] + public function create( + Request $request, + OrganizationRepository $orgaRepository, + ValidatorInterface $validator + ): Response { + /** @var string $name */ + $name = $request->request->get('name', ''); + /** @var string $csrfToken */ + $csrfToken = $request->request->get('_csrf_token', ''); + + if (!$this->isCsrfTokenValid('create organization', $csrfToken)) { + return $this->renderBadRequest('organizations/new.html.twig', [ + 'name' => $name, + 'error' => $this->csrfError(), + ]); + } + + $organization = new Organization(); + $organization->setName($name); + + $errors = $validator->validate($organization); + if (count($errors) > 0) { + return $this->renderBadRequest('organizations/new.html.twig', [ + 'name' => $name, + 'errors' => $this->formatErrors($errors), + ]); + } + + $orgaRepository->save($organization, true); + + return $this->redirectToRoute('organizations'); + } +} diff --git a/templates/home/show.html.twig b/templates/home/show.html.twig deleted file mode 100644 index 7575bf56..00000000 --- a/templates/home/show.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{# - # This file is part of Bileto. - # Copyright 2022 Probesys - # SPDX-License-Identifier: AGPL-3.0-or-later - #} - -{% extends 'base.html.twig' %} - -{% block body %} -
-

{{ 'Hello Bileto!' | trans }}

-
-{% endblock %} diff --git a/templates/organizations/index.html.twig b/templates/organizations/index.html.twig new file mode 100644 index 00000000..e4fab9d9 --- /dev/null +++ b/templates/organizations/index.html.twig @@ -0,0 +1,45 @@ +{# + # This file is part of Bileto. + # Copyright 2022 Probesys + # SPDX-License-Identifier: AGPL-3.0-or-later + #} + +{% extends 'base.html.twig' %} + +{% block title %}{{ 'Organizations' | trans }}{% endblock %} + +{% block body %} +
+

{{ 'Organizations' | trans }}

+ +
+ {% if organizations %} + + {{ 'New organization' | trans }} + + +
+ {% for organization in organizations %} +
+
+ {{ organization.name }} +
+
+ {% endfor %} +
+ {% else %} +
+

+ {{ 'You have not yet created an organization.' | trans }} +

+ +

+ + {{ 'New organization' | trans }} + +

+
+ {% endif %} +
+
+{% endblock %} diff --git a/templates/organizations/new.html.twig b/templates/organizations/new.html.twig new file mode 100644 index 00000000..4026b380 --- /dev/null +++ b/templates/organizations/new.html.twig @@ -0,0 +1,71 @@ +{# + # This file is part of Bileto. + # Copyright 2022 Probesys + # SPDX-License-Identifier: AGPL-3.0-or-later + #} + +{% extends 'base.html.twig' %} + +{% block title %}{{ 'New organization' | trans }}{% endblock %} + +{% block back %} + + {{ 'Back (organizations)' | trans }} + +{% endblock %} + +{% block body %} +
+

{{ 'New organization' | trans }}

+ + + + + {% if error %} + + {% endif %} + +
+ + + {% if errors.name is defined %} + + {% endif %} + + +
+ +
+ +
+ +
+{% endblock %} diff --git a/tests/Controller/HomeControllerTest.php b/tests/Controller/HomeControllerTest.php index d38b2060..87d811d4 100644 --- a/tests/Controller/HomeControllerTest.php +++ b/tests/Controller/HomeControllerTest.php @@ -16,7 +16,7 @@ class HomeControllerTest extends WebTestCase use Factories; use ResetDatabase; - public function testShowRendersCorrectly(): void + public function testShowRedirectsToOrganizationsIfConnected(): void { $client = static::createClient(); $user = UserFactory::createOne(); @@ -24,8 +24,7 @@ public function testShowRendersCorrectly(): void $crawler = $client->request('GET', '/'); - $this->assertResponseIsSuccessful(); - $this->assertSelectorTextContains('h1', 'Hello Bileto!'); + $this->assertResponseRedirects('/organizations', 302); } public function testShowRedirectsToLoginIfNotConnected(): void diff --git a/tests/Controller/LoginControllerTest.php b/tests/Controller/LoginControllerTest.php index 5611f9ed..d0f60520 100644 --- a/tests/Controller/LoginControllerTest.php +++ b/tests/Controller/LoginControllerTest.php @@ -119,7 +119,7 @@ public function testPostLogoutLogsUserOutAndRedirects(): void $user = UserFactory::createOne(); $client->loginUser($user->object()); - $client->request('GET', '/'); + $client->request('GET', '/organizations'); $client->submitForm('form-logout-submit'); $this->assertResponseRedirects('http://localhost/', 302); diff --git a/tests/Controller/OrganizationsControllerTest.php b/tests/Controller/OrganizationsControllerTest.php new file mode 100644 index 00000000..971669ea --- /dev/null +++ b/tests/Controller/OrganizationsControllerTest.php @@ -0,0 +1,170 @@ +loginUser($user->object()); + OrganizationFactory::createOne([ + 'name' => 'My organization 2', + ]); + OrganizationFactory::createOne([ + 'name' => 'My organization 1', + ]); + + $client->request('GET', '/organizations'); + + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains('h1', 'Organizations'); + $this->assertSelectorTextContains('[data-test="organization-item"]:nth-child(1)', 'My organization 1'); + $this->assertSelectorTextContains('[data-test="organization-item"]:nth-child(2)', 'My organization 2'); + } + + public function testGetIndexDisplaysAPlaceholderIfNoOrganization(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + + $client->request('GET', '/organizations'); + + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains( + '[data-test="organizations-placeholder"]', + 'You have not yet created an organization.' + ); + } + + public function testGetIndexRedirectsToLoginIfNotConnected(): void + { + $client = static::createClient(); + + $client->request('GET', '/organizations'); + + $this->assertResponseRedirects('http://localhost/login', 302); + } + + public function testGetNewRendersCorrectly(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + + $client->request('GET', '/organizations/new'); + + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains('h1', 'New organization'); + } + + public function testGetNewRedirectsToLoginIfNotConnected(): void + { + $client = static::createClient(); + + $client->request('GET', '/organizations/new'); + + $this->assertResponseRedirects('http://localhost/login', 302); + } + + public function testPostCreateCreatesAnOrganizationAndRedirects(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + $name = 'My organization'; + + $client->request('GET', '/organizations/new'); + $crawler = $client->submitForm('form-create-organization-submit', [ + 'name' => $name, + ]); + + $this->assertResponseRedirects('/organizations', 302); + $organization = OrganizationFactory::first(); + $this->assertSame($name, $organization->getName()); + } + + public function testPostCreateFailsIfNameIsEmpty(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + $name = ''; + + $client->request('GET', '/organizations/new'); + $crawler = $client->submitForm('form-create-organization-submit', [ + 'name' => $name, + ]); + + $this->assertSelectorTextContains('#name-error', 'The name is required.'); + $this->assertSame(0, OrganizationFactory::count()); + } + + public function testPostCreateFailsIfNameAlreadyExists(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + $name = 'My organization'; + OrganizationFactory::createOne([ + 'name' => $name, + ]); + + $client->request('GET', '/organizations/new'); + $crawler = $client->submitForm('form-create-organization-submit', [ + 'name' => $name, + ]); + + $this->assertSelectorTextContains('#name-error', 'The name "My organization" is already used.'); + $this->assertSame(1, OrganizationFactory::count()); + } + + public function testPostCreateFailsIfNameIsTooLong(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + $name = str_repeat('a', 256); + + $client->request('GET', '/organizations/new'); + $crawler = $client->submitForm('form-create-organization-submit', [ + 'name' => $name, + ]); + + $this->assertSelectorTextContains('#name-error', 'The name must be 255 characters maximum.'); + $this->assertSame(0, OrganizationFactory::count()); + } + + public function testPostCreateFailsIfCsrfTokenIsInvalid(): void + { + $client = static::createClient(); + $user = UserFactory::createOne(); + $client->loginUser($user->object()); + $name = 'My organization'; + + $client->request('GET', '/organizations/new'); + $crawler = $client->submitForm('form-create-organization-submit', [ + '_csrf_token' => 'not the token', + 'name' => $name, + ]); + + $this->assertSelectorTextContains('[data-test="alert-error"]', 'Invalid CSRF token.'); + $this->assertSame(0, OrganizationFactory::count()); + } +} From 533618173416d99911d6c7d71fb361e315c55687 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 14 Oct 2022 16:53:10 +0200 Subject: [PATCH 7/7] Update the translations --- translations/messages.en_GB.yaml | 7 +++++++ translations/messages.fr_FR.yaml | 9 ++++++++- translations/validators.en_GB.yaml | 3 +++ translations/validators.fr_FR.yaml | 3 +++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/translations/messages.en_GB.yaml b/translations/messages.en_GB.yaml index 48392da9..9f9a148a 100644 --- a/translations/messages.en_GB.yaml +++ b/translations/messages.en_GB.yaml @@ -5,3 +5,10 @@ Error: Error 'Email address': 'Email address' Password: Password Login: Login +Organizations: Organizations +'New organization': 'New organization' +'You have not yet created an organization.': 'You have not yet created an organization.' +'Back (organizations)': 'Back (organizations)' +'Organization name': 'Organization name' +'(max. 255 characters)': '(max. 255 characters)' +'Create the organization': 'Create the organization' diff --git a/translations/messages.fr_FR.yaml b/translations/messages.fr_FR.yaml index 660967ab..3d3298b6 100644 --- a/translations/messages.fr_FR.yaml +++ b/translations/messages.fr_FR.yaml @@ -1,7 +1,14 @@ 'Hello Bileto!': "Bonjour Bileto\_!" Logout: 'Se déconnecter' -'Log in': 'Connexion' +'Log in': Connexion Error: Erreur 'Email address': 'Adresse email' Password: 'Mot de passe' Login: 'Se connecter' +Organizations: Organisations +'New organization': 'Nouvelle organisation' +'You have not yet created an organization.': 'Vous n’avez pas encore créé d’organisation.' +'Back (organizations)': 'Retour (organisations)' +'Organization name': 'Nom de l’organisation' +'(max. 255 characters)': '(max. 255 caractères)' +'Create the organization': 'Créer l’organisation' diff --git a/translations/validators.en_GB.yaml b/translations/validators.en_GB.yaml index 3f42e402..335928b1 100644 --- a/translations/validators.en_GB.yaml +++ b/translations/validators.en_GB.yaml @@ -101,3 +101,6 @@ Error: Error 'The email {{ value }} is already used.': 'The email {{ value }} is already used.' 'The email is required.': 'The email is required.' 'The email {{ value }} is not a valid address.': 'The email {{ value }} is not a valid address.' +'The name {{ value }} is already used.': 'The name {{ value }} is already used.' +'The name is required.': 'The name is required.' +'The name must be {{ limit }} characters maximum.': 'The name must be {{ limit }} characters maximum.' diff --git a/translations/validators.fr_FR.yaml b/translations/validators.fr_FR.yaml index 44b9d5b9..88dcd7bd 100644 --- a/translations/validators.fr_FR.yaml +++ b/translations/validators.fr_FR.yaml @@ -101,3 +101,6 @@ Error: Erreur 'The email {{ value }} is already used.': 'L’email {{ value }} est déjà utilisé.' 'The email is required.': 'L’email est obligatoire.' 'The email {{ value }} is not a valid address.': 'L’email {{ value }} n’est pas une adresse valide.' +'The name {{ value }} is already used.': 'Le nom {{ value }} est déjà utilisé.' +'The name is required.': 'Le nom est obligatoire.' +'The name must be {{ limit }} characters maximum.': 'Le nom doit faire {{ limit }} caractères maximum.'