From 6abb14b3bd72a463ba82cb6fe7be985fb769d3f4 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Tue, 20 Oct 2020 21:41:19 +0200 Subject: [PATCH] Add basic way to have running entry --- composer.json | 2 +- composer.lock | 4 +- config/packages/framework.yaml | 3 +- config/routes.yaml | 16 ++- phpunit.xml.dist | 3 + src/Controller/.gitignore | 0 src/Controller/TimeController.php | 67 ++++++++++ src/Entity/Entry.php | 73 +++++++++++ src/Service/ActiveEntry.php | 62 +++++++++ templates/base.html.twig | 15 ++- templates/time/index.html.twig | 7 + templates/time/timer.html.twig | 11 ++ .../Controller/TimeControllerTest.php | 98 ++++++++++++++ tests/Unit/Entity/EntryTest.php | 98 ++++++++++++++ tests/Unit/Service/ActiveEntryTest.php | 123 ++++++++++++++++++ 15 files changed, 570 insertions(+), 12 deletions(-) delete mode 100644 src/Controller/.gitignore create mode 100644 src/Controller/TimeController.php create mode 100644 src/Entity/Entry.php create mode 100644 src/Service/ActiveEntry.php create mode 100644 templates/time/index.html.twig create mode 100644 templates/time/timer.html.twig create mode 100644 tests/Functional/Controller/TimeControllerTest.php create mode 100644 tests/Unit/Entity/EntryTest.php create mode 100644 tests/Unit/Service/ActiveEntryTest.php diff --git a/composer.json b/composer.json index b10979b..5e07b41 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/orm": "^2.7", "phpdocumentor/reflection-docblock": "^5.2", - "sensio/framework-extra-bundle": "^5.1", + "sensio/framework-extra-bundle": "^5.6", "symfony/asset": "5.1.*", "symfony/console": "5.1.*", "symfony/dotenv": "5.1.*", diff --git a/composer.lock b/composer.lock index e972a66..fe21e51 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": "619de2ebf9720ae1643b2e798c5a7355", + "content-hash": "42c5e66445821e40a4194fe5a0a8a7b7", "packages": [ { "name": "composer/package-versions-deprecated", @@ -7855,7 +7855,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.2.5", + "php": "^7.3.0", "ext-ctype": "*", "ext-iconv": "*" }, diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index cad7f78..7804996 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -7,7 +7,8 @@ framework: # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. session: - handler_id: null + handler_id: 'session.handler.native_file' + # save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%' cookie_secure: auto cookie_samesite: lax diff --git a/config/routes.yaml b/config/routes.yaml index c3283aa..7f4c926 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,3 +1,13 @@ -#index: -# path: / -# controller: App\Controller\DefaultController::index +--- +index: + path: / + controller: App\Controller\TimeController::index + +startNewEntry: + path: /start + controller: App\Controller\TimeController::start + methods: POST +stopRunningEntry: + path: /stop + controller: App\Controller\TimeController::stop + methods: POST diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d81f0c3..29f86af 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,6 +24,9 @@ src + + src/Kernel.php + diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Controller/TimeController.php b/src/Controller/TimeController.php new file mode 100644 index 0000000..895ae0a --- /dev/null +++ b/src/Controller/TimeController.php @@ -0,0 +1,67 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use App\Service\ActiveEntry; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class TimeController extends AbstractController +{ + /** + * @var ActiveEntry + */ + private $activeEntry; + + public function __construct(ActiveEntry $activeEntry) + { + $this->activeEntry = $activeEntry; + } + + public function index(): Response + { + return $this->render('time/index.html.twig', [ + 'entry' => $this->activeEntry->get(), + ]); + } + + public function start(Request $request): Response + { + $this->activeEntry->startNew( + $request->request->get('title') + ); + + return $this->redirectToRoute('index'); + } + + public function stop(): Response + { + if ($this->activeEntry->get()->isRunning() === false) { + return $this->redirectToRoute('index'); + } + + $this->activeEntry->stopRunning(); + + return $this->redirectToRoute('index'); + } +} diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php new file mode 100644 index 0000000..5a31ef9 --- /dev/null +++ b/src/Entity/Entry.php @@ -0,0 +1,73 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +class Entry +{ + /** + * @var string + */ + private $title = ''; + + /** + * @var \DateTimeImmutable|null + */ + private $start; + + /** + * @var \DateTimeImmutable|null + */ + private $stop; + + /** + * @var bool + */ + private $running = false; + + public function __construct( + string $title = '' + ) { + $this->title = $title; + } + + public function start(): void + { + $this->start = new \DateTimeImmutable(); + $this->running = true; + } + + public function stop(): void + { + $this->stop = new \DateTimeImmutable(); + $this->running = false; + } + + public function isRunning(): bool + { + return $this->running; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/src/Service/ActiveEntry.php b/src/Service/ActiveEntry.php new file mode 100644 index 0000000..22bf33e --- /dev/null +++ b/src/Service/ActiveEntry.php @@ -0,0 +1,62 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use App\Entity\Entry; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +class ActiveEntry +{ + /** + * @var SessionInterface + */ + private $session; + + public function __construct(SessionInterface $session) + { + $this->session = $session; + } + + public function startNew( + string $title + ): Entry { + $entry = new Entry( + $title + ); + $entry->start(); + + $this->session->set('runningEntry', $entry); + + return $entry; + } + + public function stopRunning(): void + { + $this->get()->stop(); + $this->session->remove('runningEntry'); + } + + public function get(): Entry + { + return $this->session->get('runningEntry') ?? new Entry(); + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 043f42d..fb2a3c0 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,12 +1,17 @@ - - + + - - {% block title %}Welcome!{% endblock %} + + Timetracking + {% block stylesheets %}{% endblock %} + - {% block body %}{% endblock %} + {% block body %} + {% block content %} + {% endblock %} + {% endblock %} {% block javascripts %}{% endblock %} diff --git a/templates/time/index.html.twig b/templates/time/index.html.twig new file mode 100644 index 0000000..88fa861 --- /dev/null +++ b/templates/time/index.html.twig @@ -0,0 +1,7 @@ +{% extends 'base.html.twig' %} + +{% block content %} +

Time entries

+ + {{ include('time/timer.html.twig') }} +{% endblock %} diff --git a/templates/time/timer.html.twig b/templates/time/timer.html.twig new file mode 100644 index 0000000..ba6b8b1 --- /dev/null +++ b/templates/time/timer.html.twig @@ -0,0 +1,11 @@ +{% if entry.isRunning %} +
+ + +
+{% else %} +
+ + +
+{% endif %} diff --git a/tests/Functional/Controller/TimeControllerTest.php b/tests/Functional/Controller/TimeControllerTest.php new file mode 100644 index 0000000..f5d4af4 --- /dev/null +++ b/tests/Functional/Controller/TimeControllerTest.php @@ -0,0 +1,98 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; + +/** + * @covers App\Controller\TimeController + * @uses App\Entity\Entry + * @uses App\Service\ActiveEntry + */ +class TimeControllerTest extends WebTestCase +{ + /** + * @test + */ + public function rootCanBeOpened() + { + $client = static::createClient(); + $crawler = $client->request('GET', '/'); + + $this->assertResponseIsSuccessful(); + $this->assertSelectorTextContains('h1', 'Time entries'); + } + + /** + * @test + */ + public function newEntryCanBeStarted() + { + $client = static::createClient(); + + $client->request('GET', '/'); + + $client->submitForm('Start', ['title' => 'Test Entry']); + $this->assertResponseRedirects('/'); + + $client->request('GET', '/'); + $this->assertResponseIsSuccessful(); + $this->assertInputValueSame('title', 'Test Entry'); + } + + /** + * @test + */ + public function runningEntryCanBeStopped() + { + $client = static::createClient(); + + $client->request('GET', '/'); + + $client->submitForm('Start', ['title' => 'Test Entry']); + + $client->request('GET', '/'); + $client->submitForm('Stop'); + $this->assertResponseRedirects('/'); + + $client->request('GET', '/'); + $this->assertResponseIsSuccessful(); + $this->assertInputValueSame('title', ''); + $this->assertSelectorTextSame('button', 'Start'); + } + + /** + * @test + */ + public function noneRunningEntryCanBeStopped() + { + $client = static::createClient(); + + $client->request('POST', '/stop'); + $this->assertResponseRedirects('/'); + + $client->request('GET', '/'); + $this->assertResponseIsSuccessful(); + $this->assertInputValueSame('title', ''); + $this->assertSelectorTextSame('button', 'Start'); + } +} diff --git a/tests/Unit/Entity/EntryTest.php b/tests/Unit/Entity/EntryTest.php new file mode 100644 index 0000000..cca49ff --- /dev/null +++ b/tests/Unit/Entity/EntryTest.php @@ -0,0 +1,98 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use App\Entity\Entry; +use PHPUnit\Framework\TestCase; + +/** + * @covers App\Entity\Entry + */ +class EntryTest extends TestCase +{ + /** + * @test + */ + public function canBeCreated() + { + $subject = new Entry(); + + static::assertInstanceOf(Entry::class, $subject); + } + + /** + * @test + */ + public function canBeCreatedWithTitle() + { + $subject = new Entry('Example title'); + + static::assertInstanceOf(Entry::class, $subject); + } + + /** + * @test + */ + public function returnsProvidedTitle() + { + $subject = new Entry('Example title'); + + static::assertSame('Example title', $subject->getTitle()); + } + + /** + * @test + */ + public function canBeStarted() + { + $subject = new Entry(); + + $subject->start(); + + static::assertTrue($subject->isRunning()); + } + + /** + * @test + */ + public function canBeStopped() + { + $subject = new Entry(); + + $subject->start(); + $subject->stop(); + + static::assertFalse($subject->isRunning()); + } + + /** + * @test + */ + public function canBeStoppedEvenIfNotRunning() + { + $subject = new Entry(); + + $subject->stop(); + + static::assertFalse($subject->isRunning()); + } +} diff --git a/tests/Unit/Service/ActiveEntryTest.php b/tests/Unit/Service/ActiveEntryTest.php new file mode 100644 index 0000000..e024407 --- /dev/null +++ b/tests/Unit/Service/ActiveEntryTest.php @@ -0,0 +1,123 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use App\Entity\Entry; +use App\Service\ActiveEntry; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + + +/** + * @covers App\Service\ActiveEntry + */ +class ActiveEntryTest extends TestCase +{ + /** + * @test + */ + public function canBeCreated() + { + $session = $this->prophesize(SessionInterface::class); + $subject = new ActiveEntry( + $session->reveal() + ); + + static::assertInstanceOf(ActiveEntry::class, $subject); + } + + /** + * @test + */ + public function returnsEmptyEntryAsDefault() + { + $session = $this->prophesize(SessionInterface::class); + $subject = new ActiveEntry( + $session->reveal() + ); + + $entry = $subject->get(); + + static::assertInstanceOf(Entry::class, $entry); + static::assertSame('', $entry->getTitle()); + static::assertFalse($entry->isRunning()); + } + + /** + * @test + */ + public function canStartANewEntry() + { + $session = $this->prophesize(SessionInterface::class); + $subject = new ActiveEntry( + $session->reveal() + ); + + $subject->startNew('Example title'); + + $session->set('runningEntry', Argument::that(function (Entry $entry) { + return $entry->getTitle() === 'Example title' + && $entry->isRunning() + ; + }))->shouldBeCalled(); + } + + /** + * @test + */ + public function returnsStartedEntry() + { + $session = $this->prophesize(SessionInterface::class); + $entry = $this->prophesize(Entry::class); + $subject = new ActiveEntry( + $session->reveal() + ); + + $session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled(); + $session->get('runningEntry')->willReturn($entry->reveal())->shouldBeCalled(); + + $subject->startNew('Example title'); + + static::assertSame($entry->reveal(), $subject->get()); + } + + /** + * @test + */ + public function stopsRunningEntry() + { + $session = $this->prophesize(SessionInterface::class); + $entry = $this->prophesize(Entry::class); + $subject = new ActiveEntry( + $session->reveal() + ); + + $session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled(); + $session->get('runningEntry')->willReturn($entry->reveal())->shouldBeCalled(); + + $subject->startNew('Example title'); + $subject->stopRunning(); + + $session->remove('runningEntry')->shouldBeCalled(); + } +}