* improved dev fixtures with more diversity, more data, better testcases, user avatars #42 * added customer stats to dashboard, fixed column length for 3 widgets #42 * fixed empty alias - display empty message for new user #42 * unified-ui for "new" toolbar icon #42 * use kimai2_ as database prefix #42 * added customer stats to dashboard, fixed column length for 3 widgets #42 * fix long project/customer names in "currently active" navbar flyout" #42 * added services config for dev environment #42 * added command to run unit tests #42 * added command to run integration tests #42 * added command to run code sniffer #42 * added phpcs and phpunit to composer #42 * created tests directory #42 * fixed code sniffer warnings #42 * added function to switch result type from Pagerfanta to QueryBuilder #42 * dramatically reduced database calls by using custom joined query #42 * added command to install kimai dependencies #42 * added dev:reset command #42
This commit is contained in:
@@ -32,4 +32,4 @@ class AppBundle extends Bundle
|
||||
|
||||
$container->addCompilerPass(new CompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,11 @@ class CreateUserCommand extends Command
|
||||
* @param RegistryInterface $registry
|
||||
* @param ValidatorInterface $validator
|
||||
*/
|
||||
public function __construct(UserPasswordEncoderInterface $encoder, RegistryInterface $registry, ValidatorInterface $validator)
|
||||
{
|
||||
public function __construct(
|
||||
UserPasswordEncoderInterface $encoder,
|
||||
RegistryInterface $registry,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->encoder = $encoder;
|
||||
$this->doctrine = $registry;
|
||||
$this->validator = $validator;
|
||||
@@ -70,7 +73,7 @@ class CreateUserCommand extends Command
|
||||
->addArgument('email', InputArgument::REQUIRED, 'Users email address (must be unique)')
|
||||
->addArgument('password', InputArgument::REQUIRED, 'Users password')
|
||||
->addArgument('language', InputArgument::OPTIONAL, 'Users language', User::DEFAULT_LANGUAGE)
|
||||
->addArgument('role', InputArgument::OPTIONAL, 'Users role (can be a comma separated list)', User::DEFAULT_ROLE)
|
||||
->addArgument('role', InputArgument::OPTIONAL, 'Users role (comma separated list)', User::DEFAULT_ROLE)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -104,10 +107,11 @@ class CreateUserCommand extends Command
|
||||
$errors = $this->validator->validate($user);
|
||||
if ($errors->count() > 0) {
|
||||
/** @var \Symfony\Component\Validator\ConstraintViolation $error */
|
||||
foreach($errors as $error) {
|
||||
foreach ($errors as $error) {
|
||||
$value = $error->getInvalidValue();
|
||||
$io->error(
|
||||
$error->getPropertyPath()
|
||||
. " (" . (is_array($error->getInvalidValue()) ? implode(',', $error->getInvalidValue()) : $error->getInvalidValue()).")"
|
||||
. " (" . (is_array($value) ? implode(',', $value) : $value) .")"
|
||||
. "\n "
|
||||
. $error->getMessage()
|
||||
);
|
||||
|
||||
131
src/AppBundle/Command/DevResetCommand.php
Normal file
131
src/AppBundle/Command/DevResetCommand.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class DevResetCommand extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:dev:reset')
|
||||
->setDescription('Resets the dev environment')
|
||||
->setHelp(<<<EOT
|
||||
This command will drop and re-create the database and its schemas, load development fixtures and clear the cache.
|
||||
Use the <info>-n</info> switch to skip the question.
|
||||
EOT
|
||||
)
|
||||
->addOption('no-cache', null, InputOption::VALUE_NONE, 'Skip cache flushing')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int|null
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ($input->isInteractive()) {
|
||||
if (!$this->askConfirmation(
|
||||
$input,
|
||||
$output,
|
||||
'<question>Careful, database will be purged. Do you want to continue y/N ?</question>',
|
||||
false
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:database:create');
|
||||
try {
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create database: ' . $ex->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:schema:drop');
|
||||
try {
|
||||
$command->run(new ArrayInput(['--force' => true]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to drop database schema: ' . $ex->getMessage());
|
||||
return 2;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:schema:create');
|
||||
try {
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to create database schema: ' . $ex->getMessage());
|
||||
return 3;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('doctrine:fixtures:load');
|
||||
try {
|
||||
$cmdInput = new ArrayInput([]);
|
||||
$cmdInput->setInteractive(false);
|
||||
$command->run($cmdInput, $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to import fixtures: ' . $ex->getMessage());
|
||||
return 4;
|
||||
}
|
||||
|
||||
if ($input->getOption('no-cache')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$command = $this->getApplication()->find('cache:clear');
|
||||
try {
|
||||
$command->run(new ArrayInput([]), $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to clear cache: ' . $ex->getMessage());
|
||||
return 5;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param string $question
|
||||
* @param bool $default
|
||||
* @return bool
|
||||
*/
|
||||
private function askConfirmation(InputInterface $input, OutputInterface $output, $question, $default)
|
||||
{
|
||||
$questionHelper = $this->getHelperSet()->get('question');
|
||||
$question = new ConfirmationQuestion($question, $default);
|
||||
|
||||
return $questionHelper->ask($input, $output, $question);
|
||||
}
|
||||
}
|
||||
92
src/AppBundle/Command/InstallCommand.php
Normal file
92
src/AppBundle/Command/InstallCommand.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:install')
|
||||
->setDescription('Execute all the basic installation tasks')
|
||||
->setHelp('This command will bootstrap Kimai, copies asset installation by default')
|
||||
->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlinks the assets instead of copying it')
|
||||
->addOption('relative', null, InputOption::VALUE_NONE, 'Make relative symlinks')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$arguments = [];
|
||||
|
||||
if ($input->getOption('relative')) {
|
||||
$arguments = [
|
||||
'--relative' => true,
|
||||
];
|
||||
} elseif ($input->getOption('symlink')) {
|
||||
$arguments = [
|
||||
'--symlink' => true,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->installAssets($output, $io, 'assets:install', $arguments)) {
|
||||
$this->installAssets($output, $io, 'avanzu:admin:initialize', $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputInterface $output
|
||||
* @param SymfonyStyle $io
|
||||
* @param string $cmdName
|
||||
* @param array $args
|
||||
* @return bool
|
||||
*/
|
||||
protected function installAssets(OutputInterface $output, SymfonyStyle $io, $cmdName, $args = [])
|
||||
{
|
||||
$command = $this->getApplication()->find($cmdName);
|
||||
|
||||
try {
|
||||
$returnCode = $command->run(new ArrayInput($args), $output);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Failed to install assets via "'.$cmdName.'": ' . $ex->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($returnCode != 0) {
|
||||
$io->error('Failed to install assets via "'.$cmdName.'"');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
84
src/AppBundle/Command/RunCodeSnifferCommand.php
Normal file
84
src/AppBundle/Command/RunCodeSnifferCommand.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to check the project coding styles.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class RunCodeSnifferCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir;
|
||||
|
||||
/**
|
||||
* RunCodeSnifferCommand constructor.
|
||||
* @param $rootDir
|
||||
*/
|
||||
public function __construct($rootDir)
|
||||
{
|
||||
$this->rootDir = realpath($rootDir . '/../');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:dev:phpcs')
|
||||
->setDescription('Run PHP_CodeSniffer to check for the projects coding style')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->executeCodeSniffer($io, '/src');
|
||||
$this->executeCodeSniffer($io, '/tests');
|
||||
$this->executeCodeSniffer($io, '/app/Resources/views');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
*/
|
||||
protected function executeCodeSniffer(SymfonyStyle $io, $directory)
|
||||
{
|
||||
$directory = $this->rootDir . $directory;
|
||||
|
||||
$exitCode = 0;
|
||||
ob_start();
|
||||
passthru($this->rootDir . '/bin/phpcs --standard=PSR2 ' . $directory, $exitCode);
|
||||
$result = ob_get_clean();
|
||||
|
||||
$io->write($result);
|
||||
|
||||
if ($exitCode > 0) {
|
||||
$io->error('Found problems while checking sources at: ' . $directory);
|
||||
} else {
|
||||
$io->success('All sources look good at: ' . $directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/AppBundle/Command/RunIntegrationTestsCommand.php
Normal file
46
src/AppBundle/Command/RunIntegrationTestsCommand.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to run all integration tests.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class RunIntegrationTestsCommand extends RunUnitTestsCommand
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:dev:test-integration')
|
||||
->setDescription('Run all integration tests')
|
||||
->setHelp('This command will execute all integration tests with the annotation "@group integration".')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $directory
|
||||
* @return string
|
||||
*/
|
||||
protected function createPhpunitCmdLine($directory)
|
||||
{
|
||||
return $this->rootDir . '/bin/phpunit --group integration ' . $directory;
|
||||
}
|
||||
}
|
||||
94
src/AppBundle/Command/RunUnitTestsCommand.php
Normal file
94
src/AppBundle/Command/RunUnitTestsCommand.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Command used to run all unit tests.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class RunUnitTestsCommand extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $rootDir;
|
||||
|
||||
/**
|
||||
* RunCodeSnifferCommand constructor.
|
||||
* @param $rootDir
|
||||
*/
|
||||
public function __construct($rootDir)
|
||||
{
|
||||
$this->rootDir = realpath($rootDir . '/../');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:dev:test-unit')
|
||||
->setDescription('Run all unit tests')
|
||||
->setHelp('This command will execute all unit tests. Skips all tests with "@group integration" annotation.')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->executeTests($io, '/tests/AppBundle');
|
||||
$this->executeTests($io, '/tests/TimesheetBundle');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $directory
|
||||
* @return string
|
||||
*/
|
||||
protected function createPhpunitCmdLine($directory)
|
||||
{
|
||||
return $this->rootDir . '/bin/phpunit --exclude-group integration ' . $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $directory
|
||||
*/
|
||||
protected function executeTests(SymfonyStyle $io, $directory)
|
||||
{
|
||||
$directory = $this->rootDir . $directory;
|
||||
|
||||
$exitCode = 0;
|
||||
ob_start();
|
||||
passthru($this->createPhpunitCmdLine($directory), $exitCode);
|
||||
$result = ob_get_clean();
|
||||
|
||||
$io->write($result);
|
||||
|
||||
if ($exitCode > 0) {
|
||||
$io->error('Found problems while running tests at: ' . $directory);
|
||||
} else {
|
||||
$io->success('All tests performed good at: ' . $directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,13 +43,13 @@ abstract class AbstractController extends Controller
|
||||
*
|
||||
* @param $attributes
|
||||
* @param null $subject
|
||||
* @param string $translationKey
|
||||
* @param string $translation
|
||||
* @param array $parameter
|
||||
* @throws AccessDeniedException
|
||||
*/
|
||||
protected function denyUnlessGranted($attributes, $subject = null, $translationKey = 'access.denied', $parameter = [])
|
||||
protected function denyUnlessGranted($attributes, $subject = null, $translation = 'access.denied', $parameter = [])
|
||||
{
|
||||
$error = $this->getTranslator()->trans($translationKey, $parameter, self::DOMAIN_ERROR);
|
||||
$error = $this->getTranslator()->trans($translation, $parameter, self::DOMAIN_ERROR);
|
||||
// TODO try & catch and add to audit log?
|
||||
$this->denyAccessUnlessGranted($attributes, $subject, $error);
|
||||
}
|
||||
|
||||
@@ -75,9 +75,7 @@ class UserController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'user_profile_edit', ['username' => $user->getUsername()]
|
||||
);
|
||||
return $this->redirectToRoute('user_profile_edit', ['username' => $user->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
@@ -87,7 +85,6 @@ class UserController extends AbstractController
|
||||
'form' => $editForm->createView()
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,18 +12,14 @@
|
||||
namespace AppBundle\Controller;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use AppBundle\Model\UserStatistic;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Entity\Customer;
|
||||
use TimesheetBundle\Entity\Project;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use TimesheetBundle\Model\ActivityStatistic;
|
||||
use TimesheetBundle\Model\ProjectStatistic;
|
||||
use TimesheetBundle\Model\TimesheetGlobalStatistic;
|
||||
use TimesheetBundle\Model\TimesheetStatistic;
|
||||
|
||||
/**
|
||||
* Dashboard controller for the admin area.
|
||||
@@ -49,25 +45,29 @@ class DashboardController extends Controller
|
||||
|
||||
$activityStats = $this->getDoctrine()->getRepository(Activity::class)->getGlobalStatistics();
|
||||
$projectStats = $this->getDoctrine()->getRepository(Project::class)->getGlobalStatistics();
|
||||
$customerStats = $this->getDoctrine()->getRepository(Customer::class)->getGlobalStatistics();
|
||||
$userStats = $this->getDoctrine()->getRepository(User::class)->getGlobalStatistics();
|
||||
|
||||
return $this->render('dashboard/index.html.twig', [
|
||||
'dashboard_widgets' => $this->getWidgets($timesheetUserStats, $timesheetGlobalStats, $activityStats, $projectStats, $userStats),
|
||||
'dashboard_widgets' => $this->getWidgets(),
|
||||
'timesheetGlobal' => $timesheetGlobalStats,
|
||||
'timesheetUser' => $timesheetUserStats,
|
||||
'activity' => $activityStats,
|
||||
'project' => $projectStats,
|
||||
'customer' => $customerStats,
|
||||
'user' => $userStats,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getWidgets(
|
||||
TimesheetStatistic $timesheetUserStats,
|
||||
TimesheetGlobalStatistic $timesheetGlobalStats,
|
||||
ActivityStatistic $activityStats,
|
||||
ProjectStatistic $projectStats,
|
||||
UserStatistic $userStats
|
||||
) {
|
||||
/**
|
||||
* colors: blue / yellow / purple / green / black
|
||||
* icons: bar-chart / line-chart / calendar / clock-o
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getWidgets()
|
||||
{
|
||||
// @codingStandardsIgnoreStart
|
||||
$widgets = [
|
||||
/*
|
||||
[
|
||||
@@ -104,6 +104,7 @@ class DashboardController extends Controller
|
||||
//"{{ widgets.info_box_counter('stats.amountThisMonth', timesheetGlobal.amountThisMonth|money, 'money', 'green') }}",
|
||||
"{{ widgets.info_box_counter('stats.durationTotal', timesheetGlobal.durationTotal|duration(true), 'hourglass-o', 'yellow') }}",
|
||||
//"{{ widgets.info_box_counter('stats.amountTotal', timesheetGlobal.amountTotal|money, 'money', 'red') }}",
|
||||
"{{ widgets.info_box_counter('stats.activeRecordings', timesheetGlobal.activeCurrently, 'hourglass-o', 'red', path('admin_timesheet', {'state': 1})) }}",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -111,10 +112,9 @@ class DashboardController extends Controller
|
||||
'id' => 'user.stats',
|
||||
'header' => '',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_counter('stats.userTotal', user.totalAmount, 'users', 'red') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveThisMoth', timesheetGlobal.activeThisMonth, 'users', 'yellow') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveEver', timesheetGlobal.activeTotal, 'users', 'blue') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveNow', timesheetGlobal.activeCurrently, 'users', 'green') }}",
|
||||
"{{ widgets.info_box_counter('stats.userTotal', user.totalAmount, 'user', 'red') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveThisMoth', timesheetGlobal.activeThisMonth, 'user', 'yellow') }}",
|
||||
"{{ widgets.info_box_counter('stats.userActiveEver', timesheetGlobal.activeTotal, 'user', 'blue') }}",
|
||||
],
|
||||
];
|
||||
|
||||
@@ -126,12 +126,13 @@ class DashboardController extends Controller
|
||||
'id' => 'admin.stats',
|
||||
'header' => 'dashboard.admin',
|
||||
'widgets' => [
|
||||
"{{ widgets.info_box_more('stats.activitiesTotal', activity.totalAmount, '', path('admin_activity'), 'tasks', 'purple') }}",
|
||||
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'user') }}",
|
||||
"{{ widgets.info_box_more('stats.customerTotal', customer.totalAmount, '', path('admin_customer'), 'users', 'blue') }}",
|
||||
"{{ widgets.info_box_more('stats.projectsTotal', project.totalAmount, '', path('admin_project'), 'book', 'yellow') }}",
|
||||
"{{ widgets.info_box_more('stats.userTotal', user.totalAmount, ' ', path('admin_user'), 'users') }}",
|
||||
"{{ widgets.info_box_more('stats.userActiveNow', timesheetGlobal.activeCurrently, '', path('admin_timesheet', {'state': 1}), 'hourglass-o', 'red') }}", // FIXME ???
|
||||
"{{ widgets.info_box_more('stats.activitiesTotal', activity.totalAmount, '', path('admin_activity'), 'tasks', 'purple') }}",
|
||||
],
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
||||
return $widgets;
|
||||
}
|
||||
|
||||
@@ -60,9 +60,7 @@ class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'user_profile', ['username' => $profile->getUsername()]
|
||||
);
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, $editForm, null, null, 'profile');
|
||||
@@ -89,9 +87,7 @@ class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'user_profile', ['username' => $profile->getUsername()]
|
||||
);
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, null, $pwdForm, null, 'password');
|
||||
@@ -114,9 +110,7 @@ class ProfileController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'user_profile', ['username' => $profile->getUsername()]
|
||||
);
|
||||
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
|
||||
}
|
||||
|
||||
return $this->getProfileView($profile, null, null, $rolesForm, 'roles');
|
||||
@@ -145,8 +139,13 @@ class ProfileController extends AbstractController
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
protected function getProfileView(User $user, Form $editForm = null, Form $pwdForm = null, Form $rolesForm = null, $tab = 'charts')
|
||||
{
|
||||
protected function getProfileView(
|
||||
User $user,
|
||||
Form $editForm = null,
|
||||
Form $pwdForm = null,
|
||||
Form $rolesForm = null,
|
||||
$tab = 'charts'
|
||||
) {
|
||||
/* @var $timesheetRepo TimesheetRepository */
|
||||
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
|
||||
$userStats = $timesheetRepo->getUserStatistics($user);
|
||||
|
||||
@@ -24,6 +24,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
*/
|
||||
class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
{
|
||||
|
||||
const DEFAULT_PASSWORD = 'kitten';
|
||||
|
||||
/** @var ContainerInterface */
|
||||
private $container;
|
||||
|
||||
@@ -45,18 +48,18 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
$claraCustomer->setUsername('clara_customer');
|
||||
$claraCustomer->setEmail('clara_customer@example.com');
|
||||
$claraCustomer->setRoles(['ROLE_CUSTOMER']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($claraCustomer, 'kitten');
|
||||
$claraCustomer->setPassword($encodedPassword);
|
||||
$claraCustomer->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=monsterid&f=y');
|
||||
$claraCustomer->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD));
|
||||
$manager->persist($claraCustomer);
|
||||
|
||||
$johnUser = new User();
|
||||
$johnUser->setAlias('John Doe');
|
||||
$johnUser->setTitle('Lead Developer');
|
||||
$johnUser->setTitle('Developer');
|
||||
$johnUser->setUsername('john_user');
|
||||
$johnUser->setEmail('john_user@example.com');
|
||||
$johnUser->setRoles(['ROLE_USER']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($johnUser, 'kitten');
|
||||
$johnUser->setPassword($encodedPassword);
|
||||
$johnUser->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=retro&f=y');
|
||||
$johnUser->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD));
|
||||
$manager->persist($johnUser);
|
||||
|
||||
$tonyTeamlead = new User();
|
||||
@@ -65,8 +68,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
$tonyTeamlead->setUsername('tony_teamlead');
|
||||
$tonyTeamlead->setEmail('tony_teamlead@example.com');
|
||||
$tonyTeamlead->setRoles(['ROLE_TEAMLEAD']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($tonyTeamlead, 'kitten');
|
||||
$tonyTeamlead->setPassword($encodedPassword);
|
||||
$tonyTeamlead->setAvatar('https://en.gravatar.com/userimage/3533186/bf2163b1dd23f3107a028af0195624e9.jpeg');
|
||||
$tonyTeamlead->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD));
|
||||
$manager->persist($tonyTeamlead);
|
||||
|
||||
$annaAdmin = new User();
|
||||
@@ -75,8 +78,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
$annaAdmin->setUsername('anna_admin');
|
||||
$annaAdmin->setEmail('anna_admin@example.com');
|
||||
$annaAdmin->setRoles(['ROLE_ADMIN']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($annaAdmin, 'kitten');
|
||||
$annaAdmin->setPassword($encodedPassword);
|
||||
// no avatar to test default image!
|
||||
$annaAdmin->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD));
|
||||
$manager->persist($annaAdmin);
|
||||
|
||||
$susanSuper = new User();
|
||||
@@ -85,8 +88,8 @@ class LoadFixtures implements FixtureInterface, ContainerAwareInterface
|
||||
$susanSuper->setUsername('susan_super');
|
||||
$susanSuper->setEmail('susan_super@example.com');
|
||||
$susanSuper->setRoles(['ROLE_SUPER_ADMIN']);
|
||||
$encodedPassword = $passwordEncoder->encodePassword($susanSuper, 'kitten');
|
||||
$susanSuper->setPassword($encodedPassword);
|
||||
$susanSuper->setAvatar('https://www.gravatar.com/avatar/00000000000000000000000000000000?d=wavatar&f=y');
|
||||
$susanSuper->setPassword($passwordEncoder->encodePassword($claraCustomer, self::DEFAULT_PASSWORD));
|
||||
$manager->persist($susanSuper);
|
||||
|
||||
$manager->flush();
|
||||
|
||||
@@ -31,4 +31,4 @@ class AppExtension extends Extension
|
||||
$classLoader = new ClassLoader('DoctrineExtensions', $extensionsDir);
|
||||
$classLoader->register();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Class Configuration
|
||||
* Class CompilerPass, dynamically loads additional doctrine functions for the configured database engine.
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
@@ -57,4 +57,4 @@ class CompilerPass implements CompilerPassInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,12 @@ class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
|
||||
$classMetadata->setPrimaryTable(['name' => $this->prefix . $classMetadata->getTableName()]);
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
|
||||
// Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
|
||||
// Check if "joinTable" exists:
|
||||
// it can be null if this field is the reverse side of a ManyToMany relationship
|
||||
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) ) {
|
||||
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
|
||||
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
|
||||
|
||||
@@ -12,7 +12,13 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
* User
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
|
||||
* @ORM\Table(name="users", uniqueConstraints={@ORM\UniqueConstraint(name="name", columns={"name"}), @ORM\UniqueConstraint(name="mail", columns={"mail"})})
|
||||
* @ORM\Table(
|
||||
* name="users",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="name", columns={"name"}),
|
||||
* @ORM\UniqueConstraint(name="mail", columns={"mail"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity("username")
|
||||
* @UniqueEntity("email")
|
||||
*
|
||||
@@ -381,4 +387,4 @@ class User implements UserInterface
|
||||
{
|
||||
return $this->getAlias() ?: $this->getUsername();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
|
||||
namespace AppBundle\Event;
|
||||
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* The ConfigureMainMenuEvent is used for populating the main navigation.
|
||||
*
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
namespace AppBundle\Event;
|
||||
|
||||
use Avanzu\AdminThemeBundle\Model\MenuItemModel;
|
||||
use Avanzu\AdminThemeBundle\Event\SidebarMenuEvent;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
@@ -41,14 +40,13 @@ abstract class ConfigureMenuEvent extends Event
|
||||
* ConfigureMenuEvent constructor.
|
||||
* @param AuthorizationChecker $auth
|
||||
* @param Request $request
|
||||
* @param MenuItemModel $menuModel
|
||||
* @param SidebarMenuEvent $event
|
||||
*/
|
||||
public function __construct(
|
||||
AuthorizationChecker $auth,
|
||||
Request $request,
|
||||
SidebarMenuEvent $event
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->auth = $auth;
|
||||
$this->request = $request;
|
||||
$this->event = $event;
|
||||
|
||||
@@ -13,7 +13,6 @@ namespace AppBundle\EventListener;
|
||||
|
||||
use AppBundle\Event\ConfigureMainMenuEvent;
|
||||
use AppBundle\Event\ConfigureAdminMenuEvent;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
|
||||
use Avanzu\AdminThemeBundle\Model\MenuItemModel;
|
||||
@@ -40,10 +39,7 @@ class MenuBuilder
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
* @param AuthorizationChecker $security
|
||||
*/
|
||||
public function __construct(
|
||||
EventDispatcherInterface $dispatcher,
|
||||
AuthorizationChecker $security
|
||||
)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationChecker $security)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->security = $security;
|
||||
@@ -102,7 +98,6 @@ class MenuBuilder
|
||||
$event->getRequest()->get('_route'),
|
||||
$event->getItems()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,16 +106,14 @@ class MenuBuilder
|
||||
*/
|
||||
protected function activateByRoute($route, $items)
|
||||
{
|
||||
foreach($items as $item) {
|
||||
if($item->hasChildren()) {
|
||||
foreach ($items as $item) {
|
||||
if ($item->hasChildren()) {
|
||||
$this->activateByRoute($route, $item->getChildren());
|
||||
}
|
||||
else {
|
||||
if($item->getRoute() == $route) {
|
||||
} else {
|
||||
if ($item->getRoute() == $route) {
|
||||
$item->setIsActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use AppBundle\Entity\User;
|
||||
use Avanzu\AdminThemeBundle\Event\ShowUserEvent;
|
||||
use Avanzu\AdminThemeBundle\Model\UserModel;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Class NavbarShowUserListener
|
||||
@@ -62,4 +61,4 @@ class NavbarShowUserListener
|
||||
|
||||
$event->setUser($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,9 @@ class RedirectToPreferredLocaleListener
|
||||
$this->defaultLocale = $defaultLocale ?: $this->locales[0];
|
||||
|
||||
if (!in_array($this->defaultLocale, $this->locales)) {
|
||||
throw new \UnexpectedValueException(sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales));
|
||||
throw new \UnexpectedValueException(
|
||||
sprintf('The default locale ("%s") must be one of "%s".', $this->defaultLocale, $locales)
|
||||
);
|
||||
}
|
||||
|
||||
// Add the default locale at the first position of the array,
|
||||
|
||||
@@ -43,6 +43,8 @@ class UserRolesType extends AbstractType
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
foreach ($this->roles as $key => $value) {
|
||||
$roles[$key] = $key;
|
||||
foreach ($value as $value2) {
|
||||
|
||||
@@ -11,14 +11,12 @@
|
||||
|
||||
namespace AppBundle\Repository;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use AppBundle\Repository\Query\BaseQuery;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use TimesheetBundle\Model\ActivityStatistic;
|
||||
|
||||
/**
|
||||
* Class AbstractRepository
|
||||
@@ -27,6 +25,19 @@ use TimesheetBundle\Model\ActivityStatistic;
|
||||
*/
|
||||
abstract class AbstractRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param BaseQuery $query
|
||||
* @return QueryBuilder|Pagerfanta
|
||||
*/
|
||||
protected function getBaseQueryResult(QueryBuilder $qb, BaseQuery $query)
|
||||
{
|
||||
if ($query->getResultType() == BaseQuery::RESULT_TYPE_PAGER) {
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query $query
|
||||
|
||||
@@ -22,6 +22,9 @@ class BaseQuery
|
||||
const DEFAULT_PAGESIZE = 25;
|
||||
const DEFAULT_PAGE = 1;
|
||||
|
||||
const RESULT_TYPE_PAGER = 'PagerFanta';
|
||||
const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
@@ -38,6 +41,10 @@ class BaseQuery
|
||||
* @var string
|
||||
*/
|
||||
protected $order = 'ASC';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $resultType = self::RESULT_TYPE_PAGER;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
@@ -116,4 +123,24 @@ class BaseQuery
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getResultType()
|
||||
{
|
||||
return $this->resultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $resultType
|
||||
* @return BaseQuery
|
||||
*/
|
||||
public function setResultType($resultType)
|
||||
{
|
||||
if (in_array($resultType, [self::RESULT_TYPE_PAGER, self::RESULT_TYPE_QUERYBUILDER])) {
|
||||
$this->resultType = $resultType;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +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\Tests\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* TODO adjust to actual app
|
||||
*
|
||||
* Functional test that implements a "smoke test" of all the public and secure
|
||||
* URLs of the application.
|
||||
* See http://symfony.com/doc/current/best_practices/tests.html#functional-tests.
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
*/
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* PHPUnit's data providers allow to execute the same tests repeated times
|
||||
* using a different set of data each time.
|
||||
* See http://symfony.com/doc/current/cookbook/form/unit_testing.html#testing-against-different-sets-of-data.
|
||||
*
|
||||
* @dataProvider getPublicUrls
|
||||
*/
|
||||
public function testPublicUrls($url)
|
||||
{
|
||||
$client = self::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$this->assertTrue(
|
||||
$client->getResponse()->isSuccessful(),
|
||||
sprintf('The %s public URL loads correctly.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The application contains a lot of secure URLs which shouldn't be
|
||||
* publicly accessible. This tests ensures that whenever a user tries to
|
||||
* access one of those pages, a redirection to the login form is performed.
|
||||
*
|
||||
* @dataProvider getSecureUrls
|
||||
*/
|
||||
public function testSecureUrls($url)
|
||||
{
|
||||
$this->markTestSkipped('No admin URLs for testing');
|
||||
$client = self::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$this->assertTrue($client->getResponse()->isRedirect());
|
||||
|
||||
$this->assertEquals(
|
||||
'http://localhost/en/login',
|
||||
$client->getResponse()->getTargetUrl(),
|
||||
sprintf('The %s secure URL redirects to the login form.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
public function getPublicUrls()
|
||||
{
|
||||
yield ['/'];
|
||||
yield ['/en/login'];
|
||||
}
|
||||
|
||||
public function getSecureUrls()
|
||||
{
|
||||
yield ['/en/admin/post/'];
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@ namespace AppBundle\Twig;
|
||||
|
||||
use AppBundle\Utils\Markdown;
|
||||
use Symfony\Component\Intl\Intl;
|
||||
use DateTime;
|
||||
use DateInterval;
|
||||
use TimesheetBundle\Entity\Customer;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
|
||||
@@ -31,19 +29,19 @@ class Extensions extends \Twig_Extension
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @var string[]
|
||||
*/
|
||||
private $locales;
|
||||
|
||||
/**
|
||||
* Extensions constructor.
|
||||
* @param Markdown $parser
|
||||
* @param $locales
|
||||
* @param string $locales
|
||||
*/
|
||||
public function __construct(Markdown $parser, $locales)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
$this->locales = $locales;
|
||||
$this->locales = explode('|', $locales);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,11 +156,9 @@ class Extensions extends \Twig_Extension
|
||||
*/
|
||||
public function getLocales()
|
||||
{
|
||||
$localeCodes = explode('|', $this->locales);
|
||||
|
||||
$locales = [];
|
||||
foreach ($localeCodes as $localeCode) {
|
||||
$locales[] = ['code' => $localeCode, 'name' => Intl::getLocaleBundle()->getLocaleName($localeCode, $localeCode)];
|
||||
foreach ($this->locales as $locale) {
|
||||
$locales[] = ['code' => $locale, 'name' => Intl::getLocaleBundle()->getLocaleName($locale, $locale)];
|
||||
}
|
||||
|
||||
return $locales;
|
||||
|
||||
@@ -38,7 +38,7 @@ class RoleValidator extends ConstraintValidator
|
||||
$roles = [$roles];
|
||||
}
|
||||
|
||||
foreach($roles as $role) {
|
||||
foreach ($roles as $role) {
|
||||
if (!is_string($role) || !in_array($role, $this->allowedRoles)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $this->formatValue($role))
|
||||
|
||||
@@ -36,7 +36,10 @@ class UserVoter extends AbstractVoter
|
||||
*/
|
||||
protected function supports($attribute, $subject)
|
||||
{
|
||||
if (!in_array($attribute, [self::VIEW, self::VIEW_ALL, self::EDIT, self::CREATE, self::ROLES, self::PASSWORD, self::DELETE])) {
|
||||
if (!in_array(
|
||||
$attribute,
|
||||
[self::VIEW, self::VIEW_ALL, self::EDIT, self::CREATE, self::ROLES, self::PASSWORD, self::DELETE]
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ class UserVoter extends AbstractVoter
|
||||
case self::CREATE:
|
||||
// create actually passes in the current user as $subject, not the new one
|
||||
case self::DELETE:
|
||||
// if we ever allow to delete user for ADMIN we have to check if the user to be deleted is not in a higher level
|
||||
// if we allow to delete user for ADMIN: make sure the user to be deleted is not in a higher level
|
||||
case self::ROLES:
|
||||
return $this->canAdminUsers($token);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ class ActivityController extends Controller
|
||||
public function recentActivitiesAction()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$activeEntries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days')); // TODO make days configurable
|
||||
// TODO make days configurable
|
||||
$activeEntries = $this->getRepository()->getRecentActivities($user, new \DateTime('-30 days'));
|
||||
|
||||
return $this->render(
|
||||
'TimesheetBundle:Navbar:recent-activities.html.twig',
|
||||
|
||||
@@ -14,14 +14,12 @@ namespace TimesheetBundle\Controller\Admin;
|
||||
use AppBundle\Controller\AbstractController;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use TimesheetBundle\Form\ActivityEditForm;
|
||||
use TimesheetBundle\Repository\ActivityRepository;
|
||||
use TimesheetBundle\Repository\Query\ActivityQuery;
|
||||
|
||||
/**
|
||||
@@ -89,9 +87,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'admin_activity', ['id' => $activity->getId()]
|
||||
);
|
||||
return $this->redirectToRoute('admin_activity', ['id' => $activity->getId()]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
@@ -105,7 +101,7 @@ class ActivityController extends AbstractController
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @return \Symfony\Component\Form\Form
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
private function createEditForm(Activity $activity)
|
||||
{
|
||||
|
||||
@@ -87,9 +87,7 @@ class CustomerController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'admin_customer', ['id' => $customer->getId()]
|
||||
);
|
||||
return $this->redirectToRoute('admin_customer', ['id' => $customer->getId()]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
|
||||
@@ -88,9 +88,7 @@ class ProjectController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'admin_project', ['id' => $project->getId()]
|
||||
);
|
||||
return $this->redirectToRoute('admin_project', ['id' => $project->getId()]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
|
||||
@@ -34,10 +34,16 @@ class TimesheetController extends AbstractController
|
||||
use TimesheetControllerTrait;
|
||||
|
||||
/**
|
||||
* This route shows all users timesheet entries.
|
||||
*
|
||||
* @Route("/", defaults={"page": 1}, name="admin_timesheet")
|
||||
* @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated")
|
||||
* @Method("GET")
|
||||
* @Cache(smaxage="10")
|
||||
*
|
||||
* @param $page
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction($page, Request $request)
|
||||
{
|
||||
@@ -62,10 +68,9 @@ class TimesheetController extends AbstractController
|
||||
* @Method({"GET"})
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function stopAction(Timesheet $entry, Request $request)
|
||||
public function stopAction(Timesheet $entry)
|
||||
{
|
||||
try {
|
||||
$this->getRepository()->stopRecording($entry);
|
||||
|
||||
@@ -89,7 +89,13 @@ class TimesheetController extends AbstractController
|
||||
|
||||
// make sure only ADMIN can stop other users entries
|
||||
if ($user->getId() !== $entry->getUser()->getId()) {
|
||||
$this->denyUnlessGranted('ROLE_ADMIN', null, 'timesheet.access.denied', ['%user%' => $user->getId(), '%entry%' => $entry->getId()]);
|
||||
// TODO move me to a voter
|
||||
$this->denyUnlessGranted(
|
||||
'ROLE_ADMIN',
|
||||
null,
|
||||
'timesheet.access.denied',
|
||||
['%user%' => $user->getId(), '%entry%' => $entry->getId()]
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -141,7 +147,13 @@ class TimesheetController extends AbstractController
|
||||
|
||||
// make sure only ADMIN can edit other users entries
|
||||
if ($user->getId() !== $entry->getUser()->getId()) {
|
||||
$this->denyUnlessGranted('ROLE_ADMIN', null, 'timesheet.access.denied', ['%user%' => $user->getId(), '%entry%' => $entry->getId()]);
|
||||
// TODO move me to a voter
|
||||
$this->denyUnlessGranted(
|
||||
'ROLE_ADMIN',
|
||||
null,
|
||||
'timesheet.access.denied',
|
||||
['%user%' => $user->getId(), '%entry%' => $entry->getId()]
|
||||
);
|
||||
}
|
||||
|
||||
$editForm = $this->createEditForm($entry, $request->get('page'));
|
||||
@@ -155,9 +167,7 @@ class TimesheetController extends AbstractController
|
||||
|
||||
$this->flashSuccess('action.updated_successfully');
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'timesheet_paginated', ['page' => $request->get('page')]
|
||||
);
|
||||
return $this->redirectToRoute('timesheet_paginated', ['page' => $request->get('page')]);
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
|
||||
@@ -71,7 +71,7 @@ trait TimesheetControllerTrait
|
||||
} else {
|
||||
$customer = null;
|
||||
}
|
||||
} else if ($customer !== null) {
|
||||
} elseif ($customer !== null) {
|
||||
$repo = $this->getDoctrine()->getRepository(Customer::class);
|
||||
$customer = $repo->getById($customer);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
namespace TimesheetBundle\DataFixtures\ORM;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use Symfony\Component\Intl\Intl;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Entity\Customer;
|
||||
use TimesheetBundle\Entity\Project;
|
||||
@@ -30,8 +29,7 @@ use AppBundle\DataFixtures\ORM\LoadFixtures as AppBundleLoadFixtures;
|
||||
*/
|
||||
class LoadFixtures extends AppBundleLoadFixtures
|
||||
{
|
||||
const AMOUNT_ACTIVITIES = 10; // maximum activites per project
|
||||
const AMOUNT_TIMESHEET = 1000; // timesheet entries total
|
||||
const AMOUNT_TIMESHEET = 5000; // timesheet entries total
|
||||
const RATE_MIN = 10; // minimum rate for one hour
|
||||
const RATE_MAX = 80; // maximum rate for one hour
|
||||
|
||||
@@ -109,9 +107,11 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
{
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$amountUser = count($allUser);
|
||||
|
||||
$allActivity = $this->getAllActivities($manager);
|
||||
|
||||
// by using array_pop we make sure that at least one activity has NO entry!
|
||||
array_pop($allActivity);
|
||||
|
||||
for ($i = 0; $i <= self::AMOUNT_TIMESHEET; $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$allUser[rand(1, $amountUser)],
|
||||
@@ -123,14 +123,18 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
$manager->persist($entry);
|
||||
}
|
||||
|
||||
// leave one running time entry for each user
|
||||
for ($i = 1; $i <= $amountUser; $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$allUser[$i],
|
||||
$allActivity[array_rand($allActivity)]
|
||||
);
|
||||
// by using array_pop we make sure that at least one user has NO running entry!
|
||||
array_pop($allUser);
|
||||
|
||||
$manager->persist($entry);
|
||||
// create active recodinge for test user
|
||||
foreach ($allUser as $id => $user) {
|
||||
for ($i = 0; $i < rand(1, 4); $i++) {
|
||||
$entry = $this->createTimesheetEntry(
|
||||
$user,
|
||||
$allActivity[array_rand($allActivity)]
|
||||
);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
@@ -174,17 +178,18 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
$amountTimezone = count($allTimezones);
|
||||
|
||||
$allCustomer = $this->getCustomers();
|
||||
$amountCustomer = count($allCustomer);
|
||||
shuffle($allCustomer);
|
||||
$i = 0;
|
||||
|
||||
for ($i = 0; $i < $amountCustomer; $i++) {
|
||||
foreach ($allCustomer as $customerName) {
|
||||
$entry = new Customer();
|
||||
$entry
|
||||
->setCurrency($this->getRandomCurrency())
|
||||
->setVat(rand(0, 30))
|
||||
->setName($allCustomer[$i])
|
||||
->setName($customerName)
|
||||
->setAddress($this->getRandomLocation())
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setVisible($i % 3 != 0)
|
||||
->setVisible($i++ % 3 != 0)
|
||||
->setTimezone($allTimezones[rand(1, $amountTimezone)]);
|
||||
|
||||
$manager->persist($entry);
|
||||
@@ -195,19 +200,21 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
private function loadProjects(ObjectManager $manager)
|
||||
{
|
||||
$allCustomer = $this->getAllCustomers($manager);
|
||||
$amountCustomer = count($allCustomer);
|
||||
|
||||
for ($i = 0; $i < $amountCustomer * 2; $i++) {
|
||||
foreach ($allCustomer as $id => $customer) {
|
||||
$projectForCustomer = rand(0, 7);
|
||||
for ($i = 0; $i < $projectForCustomer; $i++) {
|
||||
$entry = new Project();
|
||||
|
||||
$entry = new Project();
|
||||
$entry
|
||||
->setName($this->getRandomProject())
|
||||
->setBudget(rand(1000, 100000))
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setCustomer($allCustomer[($i % $amountCustomer) + 1])
|
||||
->setVisible($i % 3 != 0);
|
||||
$entry
|
||||
->setName($this->getRandomProject())
|
||||
->setBudget(rand(500, 100000))
|
||||
->setComment($this->getRandomPhrase())
|
||||
->setCustomer($customer)
|
||||
->setVisible($i % 3 != 0);
|
||||
|
||||
$manager->persist($entry);
|
||||
$manager->persist($entry);
|
||||
}
|
||||
}
|
||||
$manager->flush();
|
||||
}
|
||||
@@ -217,7 +224,7 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
$allProject = $this->getAllProjects($manager);
|
||||
|
||||
foreach ($allProject as $projectId => $project) {
|
||||
$activityCount = rand(1, self::AMOUNT_ACTIVITIES);
|
||||
$activityCount = rand(0, 10);
|
||||
for ($i = 0; $i < $activityCount; $i++) {
|
||||
$entry = new Activity();
|
||||
$entry
|
||||
@@ -238,12 +245,11 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
private function getActivities()
|
||||
{
|
||||
return [
|
||||
'Design',
|
||||
'Designing',
|
||||
'Programming',
|
||||
'Testing',
|
||||
'Documentation',
|
||||
'Pause',
|
||||
'Internal',
|
||||
'Research',
|
||||
'Meeting',
|
||||
'Hosting',
|
||||
@@ -256,6 +262,11 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
'Management',
|
||||
'Setup',
|
||||
'Planning',
|
||||
'Skiing',
|
||||
'Eating',
|
||||
'Watching TV',
|
||||
'Talking',
|
||||
'Cooking'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -282,8 +293,10 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
'Hosting & Server',
|
||||
'Customer Relations',
|
||||
'Infrastructure',
|
||||
'Princess Cat',
|
||||
'Software Upgrade',
|
||||
'Office Management',
|
||||
'Project X',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -310,10 +323,20 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
'Amsterdam',
|
||||
'London',
|
||||
'San Francisco',
|
||||
'Tokio',
|
||||
'Tokyo',
|
||||
'Berlin',
|
||||
'Sao Paulo',
|
||||
'Mexico City',
|
||||
'Moscow',
|
||||
'Sankt Petersburg',
|
||||
'Taiwan',
|
||||
'Perth',
|
||||
'Sydney',
|
||||
'Mumbai',
|
||||
'Lagos',
|
||||
'Karachi',
|
||||
'Shanghai',
|
||||
'Delhi',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -342,18 +365,34 @@ class LoadFixtures extends AppBundleLoadFixtures
|
||||
'Twitter',
|
||||
'Zend',
|
||||
'SensioLabs',
|
||||
'Samsung',
|
||||
'Huawai',
|
||||
'Yandex',
|
||||
'Baidu',
|
||||
'Alphabet',
|
||||
'Amazon.com',
|
||||
'Berkshire Hathaway',
|
||||
'Facebook',
|
||||
'ExxonMobil',
|
||||
'Nestle',
|
||||
'Johnson & Johnson',
|
||||
'Alibaba',
|
||||
'General Electric',
|
||||
'Procter & Gamble',
|
||||
'Wal-Mart Stores',
|
||||
'Novartis',
|
||||
'Coca-Cola',
|
||||
'Wikipedia',
|
||||
'Walt Disney',
|
||||
'Merck',
|
||||
'Pfizer',
|
||||
"L'Oréal Group",
|
||||
"McDonald's",
|
||||
'China Petroleum & Chemical',
|
||||
'GlaxoSmithKline'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getRandomCustomer()
|
||||
{
|
||||
$all = $this->getCustomers();
|
||||
return $all[array_rand($all)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
|
||||
@@ -71,7 +71,11 @@ class Project
|
||||
/**
|
||||
* @var Activity[]
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="TimesheetBundle\Entity\Activity", mappedBy="project", cascade={"persist", "merge", "remove"})
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="TimesheetBundle\Entity\Activity",
|
||||
* mappedBy="project",
|
||||
* cascade={"persist", "merge", "remove"}
|
||||
* )
|
||||
*/
|
||||
private $activities;
|
||||
|
||||
|
||||
@@ -18,7 +18,13 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
* Timesheet entity.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="TimesheetBundle\Repository\TimesheetRepository")
|
||||
* @ORM\Table(name="timesheet", indexes={@ORM\Index(columns={"user"}), @ORM\Index(name="activity", columns={"activity"})})
|
||||
* @ORM\Table(
|
||||
* name="timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(name="activity", columns={"activity"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
|
||||
@@ -77,4 +77,4 @@ class Menu
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Repository\ActivityRepository;
|
||||
use TimesheetBundle\Repository\Query\ActivityQuery;
|
||||
|
||||
/**
|
||||
* Custom form field type to select an activity.
|
||||
@@ -24,17 +26,6 @@ use TimesheetBundle\Entity\Activity;
|
||||
class ActivityType extends AbstractType
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param $key
|
||||
* @param $index
|
||||
* @return string
|
||||
*/
|
||||
public function groupBy(Activity $activity, $key, $index)
|
||||
{
|
||||
return $activity->getProject()->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -44,7 +35,14 @@ class ActivityType extends AbstractType
|
||||
'class' => 'TimesheetBundle:Activity',
|
||||
'choice_label' => 'name',
|
||||
'choice_value' => 'id',
|
||||
'group_by' => array($this, 'groupBy'),
|
||||
'group_by' => function (Activity $activity, $key, $index) {
|
||||
return $activity->getProject()->getName();
|
||||
},
|
||||
'query_builder' => function (ActivityRepository $repo) {
|
||||
$query = new ActivityQuery();
|
||||
$query->setResultType(ActivityQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use TimesheetBundle\Entity\Project;
|
||||
use TimesheetBundle\Repository\ProjectRepository;
|
||||
use TimesheetBundle\Repository\Query\ProjectQuery;
|
||||
|
||||
/**
|
||||
* Custom form field type to select a project.
|
||||
@@ -33,9 +35,14 @@ class ProjectType extends AbstractType
|
||||
'class' => 'TimesheetBundle:Project',
|
||||
'choice_label' => 'name',
|
||||
'choice_value' => 'id',
|
||||
'group_by' => function(Project $project, $key, $index) {
|
||||
'group_by' => function (Project $project, $key, $index) {
|
||||
return $project->getCustomer()->getName();
|
||||
},
|
||||
'query_builder' => function (ProjectRepository $repo) {
|
||||
$query = new ProjectQuery();
|
||||
$query->setResultType(ProjectQuery::RESULT_TYPE_QUERYBUILDER);
|
||||
return $repo->findByQuery($query);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class ActivityRepository extends AbstractRepository
|
||||
|
||||
$activities = [];
|
||||
/* @var Timesheet $entry */
|
||||
foreach($results as $entry) {
|
||||
foreach ($results as $entry) {
|
||||
$activities[] = $entry->getActivity();
|
||||
}
|
||||
|
||||
@@ -93,10 +93,9 @@ class ActivityRepository extends AbstractRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param ActivityQuery $query
|
||||
* @return \Pagerfanta\Pagerfanta
|
||||
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function findByQuery(ActivityQuery $query)
|
||||
{
|
||||
@@ -116,6 +115,6 @@ class ActivityRepository extends AbstractRepository
|
||||
// TODO check for visibility of customer and project
|
||||
}
|
||||
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class CustomerRepository extends AbstractRepository
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return \Pagerfanta\Pagerfanta
|
||||
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function findByQuery(CustomerQuery $query)
|
||||
{
|
||||
@@ -68,6 +68,6 @@ class CustomerRepository extends AbstractRepository
|
||||
$qb->andWhere('c.visible = 0');
|
||||
}
|
||||
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,13 +52,14 @@ class ProjectRepository extends AbstractRepository
|
||||
|
||||
/**
|
||||
* @param ProjectQuery $query
|
||||
* @return \Pagerfanta\Pagerfanta
|
||||
* @return \Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function findByQuery(ProjectQuery $query)
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
// if we join activities, the maxperpage limit will limit the list due to the raised amount of rows by projects * activities
|
||||
// if we join activities, the maxperpage limit will limit the list
|
||||
// due to the raised amount of rows by projects * activities
|
||||
$qb->select('p', 'c')
|
||||
->from('TimesheetBundle:Project', 'p')
|
||||
->join('p.customer', 'c')
|
||||
@@ -72,6 +73,6 @@ class ProjectRepository extends AbstractRepository
|
||||
// TODO check for visibility of customer
|
||||
}
|
||||
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,5 +158,4 @@ class TimesheetQuery extends BaseQuery
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
namespace TimesheetBundle\Repository;
|
||||
|
||||
use AppBundle\Entity\User;
|
||||
use AppBundle\Repository\AbstractRepository;
|
||||
use TimesheetBundle\Entity\Activity;
|
||||
use TimesheetBundle\Entity\Timesheet;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use TimesheetBundle\Model\Statistic\Month;
|
||||
use TimesheetBundle\Model\Statistic\Year;
|
||||
@@ -31,7 +29,7 @@ use TimesheetBundle\Repository\Query\TimesheetQuery;
|
||||
*
|
||||
* @author Kevin Papst <kevin@kevinpapst.de>
|
||||
*/
|
||||
class TimesheetRepository extends EntityRepository
|
||||
class TimesheetRepository extends AbstractRepository
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -89,7 +87,7 @@ class TimesheetRepository extends EntityRepository
|
||||
$end = new DateTime('last day of this month');
|
||||
$end->setTime(23, 59, 59);
|
||||
$begin = new DateTime('first day of this month');
|
||||
$begin->setTime(0,0,0);
|
||||
$begin->setTime(0, 0, 0);
|
||||
|
||||
return $this->queryTimeRange($select, $begin, $end, $user);
|
||||
}
|
||||
@@ -168,7 +166,7 @@ class TimesheetRepository extends EntityRepository
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('SUM(t.rate) as totalRate, SUM(t.duration) as totalDuration, MONTH(t.begin) as month, YEAR(t.begin) as year')
|
||||
$qb->select('SUM(t.rate) as rate, SUM(t.duration) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year')
|
||||
->from('TimesheetBundle:Timesheet', 't')
|
||||
->where($qb->expr()->gt('t.begin', '0'))
|
||||
->andWhere($qb->expr()->isNotNull('t.end'))
|
||||
@@ -183,7 +181,7 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
$years = [];
|
||||
foreach($qb->getQuery()->execute() as $statRow) {
|
||||
foreach ($qb->getQuery()->execute() as $statRow) {
|
||||
$curYear = $statRow['year'];
|
||||
|
||||
if (!isset($years[$curYear])) {
|
||||
@@ -196,8 +194,8 @@ class TimesheetRepository extends EntityRepository
|
||||
}
|
||||
|
||||
$month = new Month($statRow['month']);
|
||||
$month->setTotalDuration($statRow['totalDuration'])
|
||||
->setTotalRate($statRow['totalRate']);
|
||||
$month->setTotalDuration($statRow['duration'])
|
||||
->setTotalRate($statRow['rate']);
|
||||
$years[$curYear]->setMonth($month);
|
||||
}
|
||||
|
||||
@@ -310,21 +308,6 @@ class TimesheetRepository extends EntityRepository
|
||||
->setParameter('customer', $query->getCustomer());
|
||||
}
|
||||
|
||||
return $this->getPager($qb->getQuery(), $query->getPage(), $query->getPageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Query $query
|
||||
* @param int $page
|
||||
* @param int $maxPerPage
|
||||
* @return Pagerfanta
|
||||
*/
|
||||
protected function getPager(Query $query, $page = 1, $maxPerPage = 25)
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($query, false));
|
||||
$paginator->setMaxPerPage($maxPerPage);
|
||||
$paginator->setCurrentPage($page);
|
||||
|
||||
return $paginator;
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
'label.currency': 'hidden-xs',
|
||||
'label.visible': '',
|
||||
'label.actions': '',
|
||||
}, null, {'user-plus': path('admin_customer_create')}) }}
|
||||
}, null, {'plus-square': path('admin_customer_create')}) }}
|
||||
|
||||
{% for entry in entries %}
|
||||
<tr>
|
||||
|
||||
@@ -74,6 +74,7 @@ class ActivityVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Activity $activity, User $user, TokenInterface $token)
|
||||
@@ -88,6 +89,7 @@ class ActivityVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Activity $activity
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Activity $activity, User $user, TokenInterface $token)
|
||||
|
||||
@@ -74,6 +74,7 @@ class CustomerVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Customer $customer, User $user, TokenInterface $token)
|
||||
@@ -88,6 +89,7 @@ class CustomerVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Customer $customer, User $user, TokenInterface $token)
|
||||
|
||||
@@ -74,6 +74,7 @@ class ProjectVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canView(Project $project, User $user, TokenInterface $token)
|
||||
@@ -88,6 +89,7 @@ class ProjectVoter extends AbstractVoter
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canEdit(Project $project, User $user, TokenInterface $token)
|
||||
|
||||
Reference in New Issue
Block a user