added start and stop entries

enhanced administration of activities/projects/customers
added navbar for active records and recent activities
added edit timesheet entry form
added translation packages for errors and flashmessages
upgraded composer packages
This commit is contained in:
Kevin Papst
2018-01-04 14:06:07 +01:00
parent 3f429a3663
commit c9c82e767f
50 changed files with 973 additions and 405 deletions

View File

@@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Kimai package.
*
* (c) Kevin Papst <kevin@kevinpapst.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
/**
* The abstract base controller.
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
abstract class AbstractController extends Controller
{
const FLASH_SUCCESS = 'success';
const FLASH_WARNING = 'warning';
const FLASH_ERROR = 'error';
const DOMAIN_FLASH = 'flashmessages';
const DOMAIN_ERROR = 'exceptions';
const ROLE_ADMIN = 'ROLE_ADMIN';
/**
* @return object|\Symfony\Component\Translation\DataCollectorTranslator|\Symfony\Component\Translation\IdentityTranslator
*/
protected function getTranslator()
{
return $this->container->get('translator');
}
/**
* A translated helper for denyAccessUnlessGranted()
*
* @param $attributes
* @param null $subject
* @param string $translationKey
* @param array $parameter
* @throws AccessDeniedException
*/
protected function denyUnlessGranted($attributes, $subject = null, $translationKey = 'access.denied', $parameter = [])
{
$error = $this->getTranslator()->trans($translationKey, $parameter, self::DOMAIN_ERROR);
// TODO try & catch and add to audit log?
$this->denyAccessUnlessGranted($attributes, $subject, $error);
}
/**
* Adds a "successful" flash message to the stack.
*
* @param string $translationKey
* @param array $parameter
*/
protected function flashSuccess($translationKey, $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_SUCCESS, $translationKey);
}
/**
* Adds a "warning" flash message to the stack.
*
* @param $translationKey
* @param array $parameter
*/
protected function flashWarning($translationKey, $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_WARNING, $translationKey);
}
/**
* Adds a "error" flash message to the stack.
*
* @param $translationKey
* @param array $parameter
*/
protected function flashError($translationKey, $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_ERROR, $translationKey);
}
}

View File

@@ -33,7 +33,7 @@ use Symfony\Component\HttpFoundation\Request;
*
* @author Kevin Papst <kevin@kevinpapst.de>
*/
class ProfileController extends Controller
class ProfileController extends AbstractController
{
/**
* @Route("/{username}", name="user_profile")
@@ -48,10 +48,11 @@ class ProfileController extends Controller
/**
* @param User $user
* @param Form $editForm
* @param Form $pwdForm
* @param Form|null $editForm
* @param Form|null $pwdForm
* @param string $tab
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function getProfileView(User $user, Form $editForm = null, Form $pwdForm = null, $tab = 'charts')
{
@@ -105,7 +106,7 @@ class ProfileController extends Controller
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'action.updated_successfully');
$this->flashSuccess('action.updated_successfully');
return $this->redirectToRoute(
'user_profile', ['username' => $user->getUsername()]
@@ -135,7 +136,7 @@ class ProfileController extends Controller
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'action.updated_successfully');
$this->flashSuccess('action.updated_successfully');
return $this->redirectToRoute(
'user_profile', ['username' => $user->getUsername()]
@@ -146,7 +147,7 @@ class ProfileController extends Controller
}
/**
* FIXME
* FIXME implement profile deletion
* @Route("/{username}/delete", name="user_profile_delete")
* @Method({"GET", "POST"})
*/
@@ -174,7 +175,7 @@ class ProfileController extends Controller
// only administrator can bypass that part if the requested user is not the current user
if ($username !== $user->getUsername()) {
$this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page'); // TODO translation
$this->denyUnlessGranted('ROLE_ADMIN');
}
// if the user is not the current use, load the requested one
@@ -192,7 +193,7 @@ class ProfileController extends Controller
/**
* @param User $user
* @return \Symfony\Component\Form\Form
* @return \Symfony\Component\Form\FormInterface
*/
private function createEditForm(User $user)
{
@@ -208,7 +209,7 @@ class ProfileController extends Controller
/**
* @param User $user
* @return \Symfony\Component\Form\Form
* @return \Symfony\Component\Form\FormInterface
*/
private function createPasswordForm(User $user)
{
@@ -225,8 +226,7 @@ class ProfileController extends Controller
/**
* @param User $user
*
* @return \Symfony\Component\Form\Form The form
* @return \Symfony\Component\Form\FormInterface
*/
private function createDeleteForm(User $user)
{

View File

@@ -14,7 +14,7 @@ namespace AppBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* FIXME CAN BE REMOVED
* TODO adjust to actual app
*
* Functional test that implements a "smoke test" of all the public and secure
* URLs of the application.

View File

@@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Utils;
use AppBundle\Utils\Slugger;
/**
* FIXME CAN BE REMOVED
*
* Unit test for the application utils.
* See http://symfony.com/doc/current/book/testing.html#unit-tests
*
* Execute the application tests using this command (requires PHPUnit to be installed):
*
* $ cd your-symfony-project/
* $ phpunit -c app
*
*/
class SluggerTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getSlugs
*/
public function testSlugify($string, $slug)
{
$slugger = new Slugger();
$result = $slugger->slugify($string);
$this->assertEquals($slug, $result);
}
public function getSlugs()
{
yield ['Lorem Ipsum' , 'lorem-ipsum'];
yield [' Lorem Ipsum ' , 'lorem-ipsum'];
yield [' lOrEm iPsUm ' , 'lorem-ipsum'];
yield ['!Lorem Ipsum!' , 'lorem-ipsum'];
yield ['lorem-ipsum' , 'lorem-ipsum'];
}
}

View File

@@ -15,6 +15,8 @@ use AppBundle\Utils\Markdown;
use Symfony\Component\Intl\Intl;
use DateTime;
use DateInterval;
use TimesheetBundle\Entity\Customer;
use TimesheetBundle\Entity\Timesheet;
/**
* Multiple Twig extensions: filters and functions
@@ -52,7 +54,10 @@ class Extensions extends \Twig_Extension
return [
new \Twig_SimpleFilter('md2html', [$this, 'markdownToHtml'], ['is_safe' => ['html']]),
new \Twig_SimpleFilter('duration', array($this, 'duration')),
new \Twig_SimpleFilter('durationForEntry', array($this, 'durationForEntry')),
new \Twig_SimpleFilter('money', array($this, 'money')),
new \Twig_SimpleFilter('currency', array($this, 'currency')),
new \Twig_SimpleFilter('country', array($this, 'country')),
];
}
@@ -66,6 +71,18 @@ class Extensions extends \Twig_Extension
];
}
/**
* Returns the formatted duration for a Timesheet entry.
*
* @param Timesheet $entry
* @param bool $includeSeconds
* @return string
*/
public function durationForEntry(Timesheet $entry, $includeSeconds = false)
{
return $this->duration($entry->getDuration(), $includeSeconds);
}
/**
* Transforms seconds into a duration string.
*
@@ -91,13 +108,32 @@ class Extensions extends \Twig_Extension
return $hour . ':' . $minute . ':' . $second . ' h';
}
/**
* @param string $currency
* @return string
*/
public function currency($currency)
{
return Intl::getCurrencyBundle()->getCurrencySymbol($currency);
}
/**
* @param string $country
* @return string
*/
public function country($country)
{
return Intl::getRegionBundle()->getCountryName($country);
}
/**
* @param float $amount
* @param string $currency
* @return string
*/
public function money($amount, $currency = 'EUR')
public function money($amount, $currency = null)
{
$currency = $currency ?: Customer::DEFAULT_CURRENCY;
return round($amount) . ' ' . Intl::getCurrencyBundle()->getCurrencySymbol($currency);
}

View File

@@ -1,34 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace AppBundle\Utils;
/**
* FIXME CAN BE REMOVED
*
* This class is used to provide an example of integrating simple classes as
* services into a Symfony application.
*
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class Slugger
{
/**
* @param string $string
*
* @return string
*/
public function slugify($string)
{
return trim(preg_replace('/[^a-z0-9]+/', '-', strtolower(strip_tags($string))), '-');
}
}