* 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:
@@ -464,7 +464,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="admin_user.subtitle">
|
||||
<source>admin_user.subtitle</source>
|
||||
<target>Alle registrierten Nutzer</target>
|
||||
<target>Alle registrierten Benutzer der Zeitverwaltung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.alias">
|
||||
<source>label.alias</source>
|
||||
@@ -546,9 +546,9 @@
|
||||
<source>stats.userActiveEver</source>
|
||||
<target>Aktive Benutzer jemals</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.userActiveNow">
|
||||
<source>stats.userActiveNow</source>
|
||||
<target>Momentan aktiv</target>
|
||||
<trans-unit id="stats.activeRecordings">
|
||||
<source>stats.activeRecordings</source>
|
||||
<target>Momentan aktive Zeitmessungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.activitiesTotal">
|
||||
<source>stats.activitiesTotal</source>
|
||||
@@ -558,6 +558,10 @@
|
||||
<source>stats.projectsTotal</source>
|
||||
<target>Anzahl Projekte</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="stats.customerTotal">
|
||||
<source>stats.customerTotal</source>
|
||||
<target>Anzahl Kunden</target>
|
||||
</trans-unit>
|
||||
|
||||
<!--
|
||||
Month names
|
||||
|
||||
@@ -6,21 +6,20 @@
|
||||
|
||||
{% block main %}
|
||||
|
||||
{# blue / yellow / purple / green / black #}
|
||||
{# bar-chart / line-chart / calendar / clock-o #}
|
||||
|
||||
{% for settings in dashboard_widgets %}
|
||||
{% if settings.header %}
|
||||
{{ widgets.page_header(settings.header) }}
|
||||
{% endif %}
|
||||
|
||||
{% set columnWidth = 12 / (settings.widgets|length) %}
|
||||
{% set width = settings.widgets|length %}
|
||||
{% set rawWidth = 12 / width %}
|
||||
{% set columnWidth = rawWidth|round(0, 'floor') %}
|
||||
<div class="row">
|
||||
{% for widgetTemplate in settings.widgets %}
|
||||
<div class="col-md-{{ columnWidth }} col-sm-{{ columnWidth * 2 }} col-xs-{{ columnWidth * 4 }}">
|
||||
{% set widgetString = '{% import "macros/widgets.html.twig" as widgets %}' ~ widgetTemplate %}
|
||||
{{ include(template_from_string(widgetString)) }}
|
||||
</div>
|
||||
<div class="col-md-{{ columnWidth }} col-sm-{{ columnWidth * 2 }} col-xs-{{ columnWidth * 4 }}">
|
||||
{% set widgetString = '{% import "macros/widgets.html.twig" as widgets %}' ~ widgetTemplate %}
|
||||
{{ include(template_from_string(widgetString)) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -60,13 +60,20 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro info_box_counter(title, amount, icon, color) %}
|
||||
{% macro info_box_counter(title, amount, icon, color, url) %}
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-{{ color|default(kimai_context.box_color) }}"><i class="fa fa-{{ icon|default('flag-o') }}"></i></span>
|
||||
|
||||
<div class="info-box-content">
|
||||
{# this is a ugly hack, make me look nicely (dashboard widget with link) #}
|
||||
{% if url %}
|
||||
<a href="{{ url }}" class="small-box-footer">
|
||||
{% endif %}
|
||||
<span class="info-box-text">{{ title|trans }}</span>
|
||||
<span class="info-box-number">{{ amount }}</span>
|
||||
{% if url %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
{% block page_subtitle %}{{ 'profile.subtitle'|trans }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% import _self as widgets %}
|
||||
{% import _self as macro %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
{{ widgets.profile_box(user, stats) }}
|
||||
{{ widgets.profile_infos(user, stats) }}
|
||||
{{ macro.profile_box(user, stats) }}
|
||||
{{ macro.profile_infos(user, stats) }}
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
|
||||
@@ -28,6 +29,10 @@
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane {% if tab == "charts" %}active{% endif %}" id="charts">
|
||||
{% if years is empty %}
|
||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||
{% endif %}
|
||||
|
||||
<script type="text/javascript">
|
||||
var barChartOptions = {
|
||||
scaleBeginAtZero: true,
|
||||
@@ -148,7 +153,7 @@
|
||||
<div class="box box-primary">
|
||||
<div class="box-body box-profile">
|
||||
{{ macro.avatar(user.avatar, user.username, 'profile-user-img img-responsive img-circle') }}
|
||||
<h3 class="profile-username text-center">{{ user.alias }}</h3>
|
||||
<h3 class="profile-username text-center">{{ user.alias|default(user.username) }}</h3>
|
||||
|
||||
<p class="text-muted text-center">{{ user.title }}</p>
|
||||
|
||||
@@ -188,7 +193,7 @@
|
||||
{{ macro.avatar(user.avatar, user.username) }}
|
||||
</div>
|
||||
<!-- /.widget-user-image -->
|
||||
<h3 class="widget-user-username">{{ user.alias }} ({{ user.username }})</h3>
|
||||
<h3 class="widget-user-username">{{ user.alias|default(user.username) }}</h3>
|
||||
<h5 class="widget-user-desc">{{ user.title }}</h5>
|
||||
</div>
|
||||
<div class="box-footer no-padding">
|
||||
@@ -207,7 +212,7 @@
|
||||
|
||||
<div class="box box-widget widget-user">
|
||||
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
|
||||
<h3 class="widget-user-username">{{ user.alias }} ({{ user.username }})</h3>
|
||||
<h3 class="widget-user-username">{{ user.alias|default(user.username) }}</h3>
|
||||
<h5 class="widget-user-desc">{{ user.title }}</h5>
|
||||
</div>
|
||||
<div class="widget-user-image">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
imports:
|
||||
- { resource: config.yml }
|
||||
- { resource: services_dev.yml }
|
||||
|
||||
framework:
|
||||
router:
|
||||
|
||||
@@ -11,7 +11,7 @@ parameters:
|
||||
# database_url: 'mysql://root:pass@127.0.0.1:3306/timesheet'
|
||||
|
||||
# You can prefix all your tables to pevent collision with other software
|
||||
database_prefix: kimai_
|
||||
database_prefix: kimai2_
|
||||
|
||||
# You can create the database and load the sample data from the command line:
|
||||
#
|
||||
|
||||
@@ -6,20 +6,19 @@ security:
|
||||
|
||||
providers:
|
||||
# in this example, users are stored via Doctrine in the database
|
||||
# To see the users at src/AppBundle/DataFixtures/ORM/LoadFixtures.php
|
||||
# To load users from somewhere else: http://symfony.com/doc/current/cookbook/security/custom_provider.html
|
||||
# demo users can be found at src/AppBundle/DataFixtures/ORM/LoadFixtures.php
|
||||
database_users:
|
||||
entity: { class: AppBundle:User, property: username }
|
||||
|
||||
# http://symfony.com/doc/current/book/security.html#firewalls-authentication
|
||||
firewalls:
|
||||
secured_area:
|
||||
# this firewall applies to all URLs
|
||||
pattern: ^/
|
||||
|
||||
# does what it says
|
||||
logout_on_user_change: true
|
||||
|
||||
# this firewall applies to all URLs
|
||||
pattern: ^/
|
||||
|
||||
# but the firewall does not require login on every page
|
||||
# denying access is done in access_control or in your controllers
|
||||
anonymous: ~
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
services:
|
||||
|
||||
# ================================================================================
|
||||
# TWIG
|
||||
# ================================================================================
|
||||
|
||||
markdown:
|
||||
class: AppBundle\Utils\Markdown
|
||||
|
||||
# ================================================================================
|
||||
# TWIG
|
||||
# ================================================================================
|
||||
|
||||
app.twig.app_extension:
|
||||
public: false
|
||||
class: AppBundle\Twig\Extensions
|
||||
@@ -26,9 +26,9 @@ services:
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
|
||||
# ================================================================================
|
||||
# SECURITY
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# SECURITY
|
||||
# ================================================================================
|
||||
|
||||
# security voter to check user-profile access
|
||||
app.voter.user:
|
||||
@@ -58,9 +58,9 @@ services:
|
||||
tags:
|
||||
- { name: security.voter }
|
||||
|
||||
# ================================================================================
|
||||
# FORMS
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# FORMS
|
||||
# ================================================================================
|
||||
|
||||
# form to edit user roles
|
||||
app.admin.user_profile_roles:
|
||||
@@ -76,9 +76,9 @@ services:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupNavbar }
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
|
||||
# service that prefixes every database table
|
||||
app.tableprefix_subscriber:
|
||||
@@ -94,18 +94,18 @@ services:
|
||||
# app.post_repository:
|
||||
# class: Doctrine\ORM\EntityRepository
|
||||
# factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
# arguments: [AppBundle\Entity\Post]
|
||||
# arguments: [AppBundle\Entity\User]
|
||||
#
|
||||
# // traditional code inside a controller
|
||||
# $entityManager = $this->getDoctrine()->getManager();
|
||||
# $posts = $entityManager->getRepository('AppBundle:Post')->findAll();
|
||||
# $posts = $entityManager->getRepository('AppBundle:User')->findAll();
|
||||
#
|
||||
# // same code using repository services
|
||||
# $posts = $this->get('app.post_repository')->findAll();
|
||||
# $posts = $this->get('app.user_repository')->findAll();
|
||||
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# THEME
|
||||
# ================================================================================
|
||||
|
||||
avanzu_admin_theme.navbar_user_listener:
|
||||
class: AppBundle\EventListener\NavbarShowUserListener
|
||||
@@ -114,30 +114,30 @@ services:
|
||||
- { name: kernel.event_listener, event: theme.navbar_user, method: onShowUser }
|
||||
- { name: kernel.event_listener, event: theme.sidebar_user, method: onShowUser }
|
||||
|
||||
# avanzu_admin_theme.navbar_task_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_task_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.tasks, method: onListTasks }
|
||||
#
|
||||
# avanzu_admin_theme.navbar_notify_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_notify_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.notifications, method: onListNotifications }
|
||||
#
|
||||
# avanzu_admin_theme.navbar_msg_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_msg_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.messages, method: onListMessages }
|
||||
#
|
||||
# avanzu_admin_theme.setup_menu_listener:
|
||||
# class: "%avanzu_admin_theme.setup_menu_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu }
|
||||
# - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu }
|
||||
# avanzu_admin_theme.navbar_task_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_task_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.tasks, method: onListTasks }
|
||||
#
|
||||
# avanzu_admin_theme.navbar_notify_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_notify_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.notifications, method: onListNotifications }
|
||||
#
|
||||
# avanzu_admin_theme.navbar_msg_listener:
|
||||
# class: "%avanzu_admin_theme.navbar_msg_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.messages, method: onListMessages }
|
||||
#
|
||||
# avanzu_admin_theme.setup_menu_listener:
|
||||
# class: "%avanzu_admin_theme.setup_menu_listener.class%"
|
||||
# tags:
|
||||
# - { name: kernel.event_listener, event: theme.sidebar_setup_menu, method: onSetupMenu }
|
||||
# - { name: kernel.event_listener, event: theme.breadcrumb, method: onSetupMenu }
|
||||
|
||||
# ================================================================================
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# APPLICATION CORE
|
||||
# ================================================================================
|
||||
|
||||
# a route listener, that injects the locale through a URL directory
|
||||
app.redirect_to_preferred_locale_listener:
|
||||
@@ -153,9 +153,10 @@ services:
|
||||
- { name: kernel.event_listener, event: app.main_menu_configure, method: onMainMenuConfigure }
|
||||
- { name: kernel.event_listener, event: app.admin_menu_configure, method: onAdminMenuConfigure }
|
||||
|
||||
# ================================================================================
|
||||
# CONSOLE COMMANDS
|
||||
# ================================================================================
|
||||
# ================================================================================
|
||||
# CONSOLE COMMANDS
|
||||
# ================================================================================
|
||||
|
||||
app.command.create_user:
|
||||
class: AppBundle\Command\CreateUserCommand
|
||||
arguments: ["@security.password_encoder", "@doctrine", "@validator"]
|
||||
|
||||
32
app/config/services_dev.yml
Normal file
32
app/config/services_dev.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
imports:
|
||||
- { resource: services.yml }
|
||||
|
||||
services:
|
||||
|
||||
# ================================================================================
|
||||
# CONSOLE COMMANDS
|
||||
# ================================================================================
|
||||
|
||||
app.command.run_phpcs:
|
||||
class: AppBundle\Command\RunCodeSnifferCommand
|
||||
arguments: ["%kernel.root_dir%"]
|
||||
tags:
|
||||
- { name: console.command, command: kimai:dev:phpcs }
|
||||
|
||||
app.command.run_unit_tests:
|
||||
class: AppBundle\Command\RunUnitTestsCommand
|
||||
arguments: ["%kernel.root_dir%"]
|
||||
tags:
|
||||
- { name: console.command, command: kimai:dev:test-unit }
|
||||
|
||||
app.command.run_integration_tests:
|
||||
class: AppBundle\Command\RunIntegrationTestsCommand
|
||||
arguments: ["%kernel.root_dir%"]
|
||||
tags:
|
||||
- { name: console.command, command: kimai:dev:test-integration }
|
||||
|
||||
app.command.dev_reset:
|
||||
class: AppBundle\Command\DevResetCommand
|
||||
arguments: ["%kernel.root_dir%"]
|
||||
tags:
|
||||
- { name: console.command, command: kimai:dev:reset }
|
||||
@@ -44,7 +44,9 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"sensio/generator-bundle": "~3.0",
|
||||
"symfony/phpunit-bridge": "^3.0"
|
||||
"symfony/phpunit-bridge": "^3.0",
|
||||
"squizlabs/php_codesniffer": "3.*",
|
||||
"phpunit/phpunit": "^6"
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
|
||||
1494
composer.lock
generated
1494
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AppBundle\Tests\Controller;
|
||||
namespace KimaiTest\AppBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
@@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
* $ cd your-symfony-project/
|
||||
* $ phpunit -c app
|
||||
*
|
||||
* @group integration
|
||||
*/
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: kevin
|
||||
* Date: 07.01.18
|
||||
* Time: 11:12
|
||||
*/
|
||||
|
||||
namespace KimaiTest\TimesheetBundle\Repository\Query;
|
||||
|
||||
use AppBundle\Repository\Query\BaseQuery;
|
||||
use \PHPUnit\Framework\TestCase;
|
||||
|
||||
class TimesheetQueryTest extends TestCase
|
||||
{
|
||||
public function testBaseQueryHasOverwrittenFields()
|
||||
{
|
||||
$class = new \ReflectionClass(new BaseQuery());
|
||||
$this->assertTrue($class->hasProperty('order'));
|
||||
$this->assertTrue($class->hasProperty('orderBy'));
|
||||
}
|
||||
|
||||
public function testGetUser()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testSetUser()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testGetActivity()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testSetActivity()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testGetProject()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testSetProject()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testGetCustomer()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testSetCustomer()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testGetState()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
|
||||
public function testSetState()
|
||||
{
|
||||
$this->markTestIncomplete(__METHOD__);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,12 @@ li.messages-menu ul.menu li:hover .pull-left i {
|
||||
color: #dd4b39;
|
||||
|
||||
}
|
||||
|
||||
.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
.ticktac:hover i.running{
|
||||
color: #4ff131;
|
||||
|
||||
Reference in New Issue
Block a user