added plugin screen (#671)
This commit is contained in:
@@ -27,8 +27,13 @@ Remember to execute the necessary timezone conversion script, if you haven't upd
|
||||
**BC BREAKS**
|
||||
- in an ongoing effort to simplify future installation and upgrade processes the `.env` variable `DATABASE_PREFIX` was removed.
|
||||
The table prefix is now hardcoded to `kimai2_`. If you used another prefix, you have to rename your tables manually
|
||||
before starting the update process. You can also delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
before starting the update process. And delete the row `DATABASE_PREFIX` from your `.env` file.
|
||||
- API: DateTime objects will be returned including timezone identifier (previously 2019-03-02 14:23 - now 2019-03-02T14:23:00+00:00)
|
||||
- Plugin mechanism changed: existing Plugins have to be deleted or updated
|
||||
|
||||
**Check if you want to apply changes to your `local.yaml`:**
|
||||
|
||||
- New permissions are available: `system_configuration`, `system_actions`, `plugins`
|
||||
|
||||
**Check if you want to apply changes to your `local.yaml`:**
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
/*!
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* Main JS application file for Kimai 2. This file should be included in all pages.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"description": "Kimai Time-Tracking 2",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"homepage": "https://www.kevinpapst.de"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"ext-gd": "*",
|
||||
@@ -56,7 +62,6 @@
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.10",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"squizlabs/php_codesniffer": "^3.2",
|
||||
"symfony/browser-kit": "^4.0",
|
||||
"symfony/css-selector": "^4.0",
|
||||
"symfony/phpunit-bridge": "^4.0",
|
||||
|
||||
459
composer.lock
generated
459
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,10 @@ parameters:
|
||||
|
||||
doctrine:
|
||||
dbal:
|
||||
# configure these for your database server
|
||||
default_connection: default
|
||||
connections:
|
||||
default:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
driver: 'pdo_mysql'
|
||||
server_version: '5.7'
|
||||
charset: utf8mb4
|
||||
@@ -15,24 +18,19 @@ doctrine:
|
||||
charset: utf8mb4
|
||||
collate: utf8mb4_unicode_ci
|
||||
|
||||
# With Symfony 3.3, remove the `resolve:` prefix
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
|
||||
types:
|
||||
datetime: App\Doctrine\UTCDateTimeType
|
||||
orm:
|
||||
auto_generate_proxy_classes: '%kernel.debug%'
|
||||
default_entity_manager: default
|
||||
entity_managers:
|
||||
default:
|
||||
connection: default
|
||||
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||
auto_mapping: true
|
||||
mappings:
|
||||
App:
|
||||
is_bundle: false
|
||||
type: annotation
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: Kimai
|
||||
loggable:
|
||||
type: annotation
|
||||
alias: Gedmo
|
||||
prefix: Gedmo\Loggable\Entity
|
||||
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
|
||||
|
||||
@@ -97,7 +97,7 @@ kimai:
|
||||
ROLE_USER: []
|
||||
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
||||
ROLE_ADMIN: [hourly-rate_own_profile]
|
||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_configuration]
|
||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_actions,system_configuration,plugins]
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
# Language specific settings, like the date formats
|
||||
|
||||
@@ -36,6 +36,10 @@ security:
|
||||
path: fos_user_security_logout
|
||||
target: homepage
|
||||
|
||||
access_decision_manager:
|
||||
strategy: unanimous
|
||||
allow_if_all_abstain: false
|
||||
|
||||
role_hierarchy:
|
||||
ROLE_USER: ROLE_CUSTOMER
|
||||
ROLE_TEAMLEAD: ROLE_USER
|
||||
|
||||
@@ -64,16 +64,16 @@ services:
|
||||
App\Utils\MPdfConverter:
|
||||
arguments: ['%kernel.cache_dir%']
|
||||
|
||||
App\Plugin\PluginManager:
|
||||
arguments: [!tagged kimai.plugin]
|
||||
|
||||
App\Export\ServiceExport:
|
||||
arguments: [!tagged export.renderer]
|
||||
|
||||
# ================================================================================
|
||||
# DATABASE
|
||||
# ================================================================================
|
||||
|
||||
# service that prefixes every database table
|
||||
App\Doctrine\TablePrefixSubscriber:
|
||||
class: App\Doctrine\TablePrefixSubscriber
|
||||
tags:
|
||||
- { name: doctrine.event_subscriber }
|
||||
|
||||
# updates timesheet records and apply configured rate & rounding rules
|
||||
App\Doctrine\TimesheetSubscriber:
|
||||
class: App\Doctrine\TimesheetSubscriber
|
||||
|
||||
@@ -11,6 +11,10 @@ namespace App\Controller;
|
||||
|
||||
use App\Constants;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -40,6 +44,11 @@ class AboutController extends AbstractController
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
return $this->getAboutView();
|
||||
}
|
||||
|
||||
protected function getAboutView(array $additional = [])
|
||||
{
|
||||
$phpInfo = $this->getPhpInfo();
|
||||
unset($phpInfo[0]);
|
||||
@@ -71,7 +80,8 @@ class AboutController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('about/system.html.twig', [
|
||||
return $this->render('about/system.html.twig', array_merge(
|
||||
[
|
||||
'modules' => get_loaded_extensions(),
|
||||
'dotenv' => [
|
||||
'APP_ENV' => getenv('APP_ENV'),
|
||||
@@ -81,7 +91,9 @@ class AboutController extends AbstractController
|
||||
'info' => $phpInfo,
|
||||
'settings' => $settings,
|
||||
'license' => $this->getLicense(),
|
||||
]);
|
||||
],
|
||||
$additional
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,4 +153,26 @@ class AboutController extends AbstractController
|
||||
|
||||
return $phpinfo['phpinfo'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/flush-cache", name="system_flush_cache", methods={"GET"})
|
||||
*
|
||||
* @Security("is_granted('system_actions')")
|
||||
*/
|
||||
public function rebuildContainer(KernelInterface $kernel)
|
||||
{
|
||||
$application = new Application($kernel);
|
||||
$application->setAutoExit(false);
|
||||
|
||||
$input = new ArrayInput([
|
||||
'command' => 'cache:clear',
|
||||
'--env' => $kernel->getEnvironment(),
|
||||
'-n',
|
||||
]);
|
||||
|
||||
$output = new BufferedOutput();
|
||||
$application->run($input, $output);
|
||||
|
||||
return $this->getAboutView(['content_action' => $output->fetch()]);
|
||||
}
|
||||
}
|
||||
|
||||
51
src/Controller/PluginController.php
Normal file
51
src/Controller/PluginController.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Plugin\PluginManager;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/plugins")
|
||||
* @Security("is_granted('plugins')")
|
||||
*/
|
||||
class PluginController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var PluginManager
|
||||
*/
|
||||
protected $plugins;
|
||||
|
||||
/**
|
||||
* @param PluginManager $manager
|
||||
*/
|
||||
public function __construct(PluginManager $manager)
|
||||
{
|
||||
$this->plugins = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="plugins", methods={"GET"})
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$plugins = $this->plugins->getPlugins();
|
||||
foreach ($this->plugins->getPlugins() as $plugin) {
|
||||
$this->plugins->loadMetadata($plugin);
|
||||
}
|
||||
|
||||
return $this->render('plugin/index.html.twig', [
|
||||
'plugins' => $plugins,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,10 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @return array|false|null|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function findEngine(ContainerBuilder $container)
|
||||
protected function findEngine()
|
||||
{
|
||||
$engine = null;
|
||||
|
||||
@@ -47,7 +46,7 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
if (null === $engine) {
|
||||
throw new \Exception(
|
||||
'Could not detect database engine. Please set the environment config DATABASE_ENGINE ' .
|
||||
'to one of: "' . implode(', ', $this->allowedEngines) . '" in your .env file: DATABASE_ENGINE=sqlite'
|
||||
'to one of: "' . implode(', ', $this->allowedEngines) . '" in your .env file, e.g. DATABASE_ENGINE=sqlite'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +67,7 @@ class DoctrineCompilerPass implements CompilerPassInterface
|
||||
*/
|
||||
protected function getConfigFile(ContainerBuilder $container)
|
||||
{
|
||||
$engine = $this->findEngine($container);
|
||||
$engine = $this->findEngine();
|
||||
|
||||
$configDir = realpath(
|
||||
$container->getParameter('kernel.project_dir') . '/vendor/beberlei/DoctrineExtensions/config/'
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\DependencyInjection\Compiler;
|
||||
|
||||
use App\Export\ServiceExport;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Dynamically adds all dependencies to the ExportService.
|
||||
*/
|
||||
class ExportServiceCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
/**
|
||||
* @param ContainerBuilder $container
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
// always first check if the primary service is defined
|
||||
if (!$container->has(ServiceExport::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->findDefinition(ServiceExport::class);
|
||||
|
||||
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
|
||||
foreach ($taggedRenderer as $id => $tags) {
|
||||
$definition->addMethodCall('addRenderer', [new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,11 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
|
||||
/**
|
||||
* @param string $name
|
||||
* @return string
|
||||
* @deprecated since 0.9
|
||||
*/
|
||||
protected function getTableName($name)
|
||||
{
|
||||
return TablePrefixSubscriber::PREFIX . $name;
|
||||
return 'kimai2_' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* Adds a prefix to every doctrine entity AKA database table
|
||||
*/
|
||||
class TablePrefixSubscriber implements EventSubscriber
|
||||
{
|
||||
public const PREFIX = 'kimai2_';
|
||||
|
||||
/**
|
||||
* @return array|string[]
|
||||
*/
|
||||
public function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
Events::loadClassMetadata,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LoadClassMetadataEventArgs $args
|
||||
*/
|
||||
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
|
||||
{
|
||||
$classMetadata = $args->getClassMetadata();
|
||||
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
|
||||
// if we are in an inheritance hierarchy, only apply this once
|
||||
return;
|
||||
}
|
||||
|
||||
$classMetadata->setPrimaryTable(['name' => self::PREFIX . $classMetadata->getTableName()]);
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if (\Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY == $mapping['type']
|
||||
// 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'] = self::PREFIX . $mappedTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="activities")
|
||||
* @ORM\Table(name="kimai2_activities")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*/
|
||||
class Activity
|
||||
|
||||
@@ -15,8 +15,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")
|
||||
* @ORM\Table(
|
||||
* name="configuration",
|
||||
* @ORM\Table(name="kimai2_configuration",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="customers")
|
||||
* @ORM\Table(name="kimai2_customers")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*/
|
||||
class Customer
|
||||
|
||||
@@ -13,11 +13,8 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* InvoiceTemplate
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceTemplateRepository")
|
||||
* @ORM\Table(
|
||||
* name="invoice_templates",
|
||||
* @ORM\Table(name="kimai2_invoice_templates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
|
||||
@@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="projects")
|
||||
* @ORM\Table(name="kimai2_projects")
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
*/
|
||||
class Project
|
||||
|
||||
@@ -13,8 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(
|
||||
* name="timesheet",
|
||||
* @ORM\Table(name="kimai2_timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(columns={"activity_id"})
|
||||
|
||||
@@ -18,11 +18,8 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Application main User entity.
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\Table(
|
||||
* name="users",
|
||||
* @ORM\Table(name="kimai2_users",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"username"}),
|
||||
* @ORM\UniqueConstraint(columns={"email"})
|
||||
|
||||
@@ -16,11 +16,8 @@ use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* UserPreference
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(
|
||||
* name="user_preferences",
|
||||
* @ORM\Table(name="kimai2_user_preferences",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "name"})
|
||||
* }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Event;
|
||||
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -28,17 +29,27 @@ class ConfigureMainMenuEvent extends Event
|
||||
* @var SidebarMenuEvent
|
||||
*/
|
||||
private $event;
|
||||
/**
|
||||
* @var MenuItemModel
|
||||
*/
|
||||
private $admin;
|
||||
/**
|
||||
* @var MenuItemModel
|
||||
*/
|
||||
private $system;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param SidebarMenuEvent $event
|
||||
* @param MenuItemModel $admin
|
||||
* @param MenuItemModel $system
|
||||
*/
|
||||
public function __construct(
|
||||
Request $request,
|
||||
SidebarMenuEvent $event
|
||||
) {
|
||||
public function __construct(Request $request, SidebarMenuEvent $event, MenuItemModel $admin, MenuItemModel $system)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->event = $event;
|
||||
$this->admin = $admin;
|
||||
$this->system = $system;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,4 +67,20 @@ class ConfigureMainMenuEvent extends Event
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel
|
||||
*/
|
||||
public function getAdminMenu()
|
||||
{
|
||||
return $this->admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MenuItemModel
|
||||
*/
|
||||
public function getSystemMenu()
|
||||
{
|
||||
return $this->system;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ class ThemeEvent extends Event
|
||||
public const JAVASCRIPT = 'app.theme.javascript';
|
||||
public const STYLESHEET = 'app.theme.css';
|
||||
public const HTML_HEAD = 'app.theme.html_head';
|
||||
public const CONTENT_BEFORE = 'app.theme.content_before';
|
||||
public const CONTENT_START = 'app.theme.content_start';
|
||||
public const CONTENT_END = 'app.theme.content_end';
|
||||
public const CONTENT_AFTER = 'app.theme.content_after';
|
||||
|
||||
/**
|
||||
* @var User
|
||||
|
||||
@@ -66,26 +66,36 @@ class MenuBuilderSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('dashboard', 'menu.homepage', 'dashboard', [], 'fas fa-tachometer-alt')
|
||||
);
|
||||
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureMainMenuEvent::CONFIGURE,
|
||||
new ConfigureMainMenuEvent(
|
||||
$menuEvent = new ConfigureMainMenuEvent(
|
||||
$request,
|
||||
$event
|
||||
)
|
||||
$event,
|
||||
new MenuItemModel('admin', 'menu.admin', ''),
|
||||
new MenuItemModel('system', 'menu.system', '')
|
||||
);
|
||||
|
||||
$admin = new MenuItemModel('admin', 'menu.admin', '', [], 'fas fa-wrench');
|
||||
$this->eventDispatcher->dispatch(ConfigureMainMenuEvent::CONFIGURE, $menuEvent);
|
||||
|
||||
// @deprecated since 0.9, will be removed with 1.0
|
||||
$this->eventDispatcher->dispatch(
|
||||
ConfigureAdminMenuEvent::CONFIGURE,
|
||||
new ConfigureAdminMenuEvent(
|
||||
$request,
|
||||
$admin
|
||||
$menuEvent->getAdminMenu()
|
||||
)
|
||||
);
|
||||
|
||||
if ($admin->hasChildren()) {
|
||||
$event->addItem($admin);
|
||||
if ($menuEvent->getAdminMenu()->hasChildren()) {
|
||||
$event->addItem(new MenuItemModel('admin', 'menu.admin', ''));
|
||||
foreach ($menuEvent->getAdminMenu()->getChildren() as $child) {
|
||||
$event->addItem($child);
|
||||
}
|
||||
}
|
||||
|
||||
if ($menuEvent->getSystemMenu()->hasChildren()) {
|
||||
$event->addItem(new MenuItemModel('system', 'menu.system', ''));
|
||||
foreach ($menuEvent->getSystemMenu()->getChildren() as $child) {
|
||||
$event->addItem($child);
|
||||
}
|
||||
}
|
||||
|
||||
$this->activateByRoute(
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Event\ConfigureAdminMenuEvent;
|
||||
use App\Event\ConfigureMainMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Event\SidebarMenuEvent;
|
||||
use KevinPapst\AdminLTEBundle\Model\MenuItemModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Menu event subscriber for timesheet, customer, projects, activities.
|
||||
* This is a sample implementation for developer who want to add new navigation entries in their bundles.
|
||||
* Menu event subscriber is creating the Kimai default menu structure.
|
||||
*/
|
||||
class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
@@ -42,7 +41,6 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
ConfigureMainMenuEvent::CONFIGURE => ['onMainMenuConfigure', 100],
|
||||
ConfigureAdminMenuEvent::CONFIGURE => ['onAdminMenuConfigure', 100],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -57,7 +55,17 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getMenu();
|
||||
$this->configureMainMenu($event->getMenu());
|
||||
$this->configureAdminMenu($event->getAdminMenu());
|
||||
$this->configureSystemMenu($event->getSystemMenu());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SidebarMenuEvent $menu
|
||||
*/
|
||||
protected function configureMainMenu(SidebarMenuEvent $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if ($auth->isGranted('view_own_timesheet')) {
|
||||
$menu->addItem(
|
||||
@@ -67,7 +75,7 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
|
||||
if ($auth->isGranted('view_invoice')) {
|
||||
$menu->addItem(
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'far fa-file-alt')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,33 +87,21 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \App\Event\ConfigureAdminMenuEvent $event
|
||||
* @param MenuItemModel $menu
|
||||
*/
|
||||
public function onAdminMenuConfigure(ConfigureAdminMenuEvent $event)
|
||||
protected function configureAdminMenu(MenuItemModel $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if ($auth->isGranted('view_other_timesheet')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'far fa-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-user')
|
||||
new MenuItemModel('timesheet_admin', 'menu.admin_timesheet', 'admin_timesheet', [], 'fas fa-user-clock')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_customer')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-users')
|
||||
new MenuItemModel('customer_admin', 'menu.admin_customer', 'admin_customer', [], 'fas fa-user-tie')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,6 +116,26 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MenuItemModel $menu
|
||||
*/
|
||||
protected function configureSystemMenu(MenuItemModel $menu)
|
||||
{
|
||||
$auth = $this->security;
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('user_admin', 'menu.admin_user', 'admin_user', [], 'fas fa-users')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('plugins')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], 'fas fa-plug')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_configuration')) {
|
||||
$menu->addChild(
|
||||
|
||||
@@ -13,20 +13,21 @@ use App\Entity\Timesheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class HtmlRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
@@ -35,9 +36,9 @@ class HtmlRenderer implements RendererInterface
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
|
||||
@@ -16,13 +16,14 @@ use App\Timesheet\UserDateTimeFactory;
|
||||
use App\Utils\HtmlToPdfConverter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Twig\Environment;
|
||||
|
||||
class PDFRenderer implements RendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
/**
|
||||
@@ -35,10 +36,11 @@ class PDFRenderer implements RendererInterface
|
||||
protected $converter;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
* @param UserDateTimeFactory $dateTime
|
||||
* @param HtmlToPdfConverter $converter
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
|
||||
public function __construct(Environment $twig, UserDateTimeFactory $dateTime, HtmlToPdfConverter $converter)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
$this->dateTime = $dateTime;
|
||||
@@ -49,10 +51,9 @@ class PDFRenderer implements RendererInterface
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Mpdf\MpdfException
|
||||
* @throws \Twig_Error_Loader
|
||||
* @throws \Twig_Error_Runtime
|
||||
* @throws \Twig_Error_Syntax
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
|
||||
@@ -16,6 +16,16 @@ class ServiceExport
|
||||
*/
|
||||
protected $renderer = [];
|
||||
|
||||
/**
|
||||
* @param RendererInterface[] $renderer
|
||||
*/
|
||||
public function __construct(iterable $renderer)
|
||||
{
|
||||
foreach ($renderer as $render) {
|
||||
$this->addRenderer($render);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RendererInterface $renderer
|
||||
* @return $this
|
||||
|
||||
@@ -13,18 +13,19 @@ use App\Entity\InvoiceDocument;
|
||||
use App\Invoice\RendererInterface;
|
||||
use App\Model\InvoiceModel;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
|
||||
class TwigRenderer implements RendererInterface
|
||||
{
|
||||
/**
|
||||
* @var \Twig_Environment
|
||||
* @var Environment
|
||||
*/
|
||||
protected $twig;
|
||||
|
||||
/**
|
||||
* @param \Twig_Environment $twig
|
||||
* @param Environment $twig
|
||||
*/
|
||||
public function __construct(\Twig_Environment $twig)
|
||||
public function __construct(Environment $twig)
|
||||
{
|
||||
$this->twig = $twig;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ namespace App;
|
||||
|
||||
use App\DependencyInjection\AppExtension;
|
||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
||||
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
||||
use App\Export\RendererInterface as ExportRendererInterface;
|
||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||
use App\Invoice\NumberGeneratorInterface;
|
||||
use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||
use App\Plugin\PluginInterface;
|
||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
@@ -34,6 +34,7 @@ class Kernel extends BaseKernel
|
||||
|
||||
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||
|
||||
public const TAG_PLUGIN = 'kimai.plugin';
|
||||
public const TAG_EXPORT_RENDERER = 'export.renderer';
|
||||
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
||||
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
||||
@@ -56,6 +57,7 @@ class Kernel extends BaseKernel
|
||||
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||
$container->registerForAutoconfiguration(PluginInterface::class)->addTag(self::TAG_PLUGIN);
|
||||
}
|
||||
|
||||
public function registerBundles()
|
||||
@@ -67,6 +69,12 @@ class Kernel extends BaseKernel
|
||||
}
|
||||
}
|
||||
|
||||
// do not load Kimai plugin in test environment, they may alter the default behaviour and create
|
||||
// false-negatives in integration/system tests
|
||||
if ('test' === $this->environment) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
||||
if (!file_exists($pluginsDir)) {
|
||||
return;
|
||||
@@ -110,7 +118,6 @@ class Kernel extends BaseKernel
|
||||
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
$container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
|
||||
83
src/Plugin/Plugin.php
Normal file
83
src/Plugin/Plugin.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Plugin;
|
||||
|
||||
class Plugin
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $path;
|
||||
/**
|
||||
* @var PluginMetadata
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
/**
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function getMetadata(): ?PluginMetadata
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginMetadata $metadata
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setMetadata(PluginMetadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setPath(string $path)
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Plugin
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
23
src/Plugin/PluginInterface.php
Normal file
23
src/Plugin/PluginInterface.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Plugin;
|
||||
|
||||
interface PluginInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath();
|
||||
}
|
||||
111
src/Plugin/PluginManager.php
Normal file
111
src/Plugin/PluginManager.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Plugin;
|
||||
|
||||
use App\Constants;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
/**
|
||||
* @var Plugin[]
|
||||
*/
|
||||
private $plugins = [];
|
||||
|
||||
/**
|
||||
* @param PluginInterface[] $plugins
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(iterable $plugins)
|
||||
{
|
||||
foreach ($plugins as $plugin) {
|
||||
$this->addPlugin($plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginInterface $plugin
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function addPlugin(PluginInterface $plugin)
|
||||
{
|
||||
if (isset($this->plugins[$plugin->getName()])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->plugins[$plugin->getName()] = $this->createPlugin($plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Plugin[]
|
||||
*/
|
||||
public function getPlugins()
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Plugin|null
|
||||
*/
|
||||
public function getPlugin(string $name): ?Plugin
|
||||
{
|
||||
if (!isset($this->plugins[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->plugins[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PluginInterface $bundle
|
||||
* @return Plugin
|
||||
*/
|
||||
protected function createPlugin(PluginInterface $bundle)
|
||||
{
|
||||
$plugin = new Plugin();
|
||||
$plugin
|
||||
->setName($bundle->getName())
|
||||
->setPath($bundle->getPath())
|
||||
->setMetadata(new PluginMetadata())
|
||||
;
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method and pass a plugin, to set its metdata.
|
||||
* This is not pre-filled by default, as it would mean to parse several composer.json on each request.
|
||||
*
|
||||
* @param Plugin $plugin
|
||||
*/
|
||||
public function loadMetadata(Plugin $plugin)
|
||||
{
|
||||
$composer = $plugin->getPath() . '/composer.json';
|
||||
if (!file_exists($composer) || !is_readable($composer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = json_decode(file_get_contents($composer), true);
|
||||
|
||||
$reqVersion = $json['extra']['kimai']['require'] ?? 'unknown';
|
||||
$version = $json['extra']['kimai']['version'] ?? 'unknown';
|
||||
$description = $json['description'] ?? '';
|
||||
|
||||
$homepage = $json['homepage'] ?? Constants::HOMEPAGE . '/store/';
|
||||
|
||||
$plugin
|
||||
->getMetadata()
|
||||
->setHomepage($homepage)
|
||||
->setKimaiVersion($reqVersion)
|
||||
->setVersion($version)
|
||||
->setDescription($description)
|
||||
;
|
||||
}
|
||||
}
|
||||
106
src/Plugin/PluginMetadata.php
Normal file
106
src/Plugin/PluginMetadata.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Plugin;
|
||||
|
||||
class PluginMetadata
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $version;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $kimaiVersion;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $homepage;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setDescription(string $description)
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion(): ?string
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $version
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setVersion(string $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKimaiVersion(): ?string
|
||||
{
|
||||
return $this->kimaiVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $kimaiVersion
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setKimaiVersion(string $kimaiVersion)
|
||||
{
|
||||
$this->kimaiVersion = $kimaiVersion;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHomepage(): ?string
|
||||
{
|
||||
return $this->homepage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $homepage
|
||||
* @return PluginMetadata
|
||||
*/
|
||||
public function setHomepage(string $homepage)
|
||||
{
|
||||
$this->homepage = $homepage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,8 @@ class Extensions extends AbstractExtension
|
||||
'on' => 'fas fa-toggle-on',
|
||||
'off' => 'fas fa-toggle-off',
|
||||
'audit' => 'fas fa-history',
|
||||
'home' => 'fas fa-home',
|
||||
'shop' => 'fas fa-shopping-cart',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,7 +77,7 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
switch ($attribute) {
|
||||
case self::START:
|
||||
if (!$this->canStart($subject, $user, $token)) {
|
||||
if (!$this->canStart($subject)) {
|
||||
return false;
|
||||
}
|
||||
$permission .= $attribute;
|
||||
@@ -113,11 +113,9 @@ class TimesheetVoter extends AbstractVoter
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param User $user
|
||||
* @param TokenInterface $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function canStart(Timesheet $timesheet, User $user, TokenInterface $token)
|
||||
protected function canStart(Timesheet $timesheet)
|
||||
{
|
||||
// possible improvements for the future:
|
||||
// we could check the amount of active entries (maybe slow)
|
||||
|
||||
@@ -74,7 +74,7 @@ class UserVoter extends AbstractVoter
|
||||
switch ($attribute) {
|
||||
// special case for the UserController
|
||||
case self::DELETE:
|
||||
if (!$this->canDelete($subject, $user, $token)) {
|
||||
if (!$this->canDelete($subject, $user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class UserVoter extends AbstractVoter
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDelete(User $profile, User $user, TokenInterface $token)
|
||||
protected function canDelete(User $profile, User $user)
|
||||
{
|
||||
return $profile->getId() !== $user->getId();
|
||||
}
|
||||
|
||||
@@ -338,9 +338,6 @@
|
||||
"setasign/fpdi": {
|
||||
"version": "1.6.2"
|
||||
},
|
||||
"squizlabs/php_codesniffer": {
|
||||
"version": "3.2.2"
|
||||
},
|
||||
"swiftmailer/swiftmailer": {
|
||||
"version": "v6.0.2"
|
||||
},
|
||||
@@ -482,6 +479,9 @@
|
||||
"symfony/polyfill-intl-icu": {
|
||||
"version": "v1.6.0"
|
||||
},
|
||||
"symfony/polyfill-intl-idn": {
|
||||
"version": "v1.11.0"
|
||||
},
|
||||
"symfony/polyfill-mbstring": {
|
||||
"version": "v1.6.0"
|
||||
},
|
||||
|
||||
@@ -1,38 +1,57 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% block page_title %}{{ 'about.title'|trans({}, 'about') }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'about.subtitle'|trans({}, 'about') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{{ widgets.callout('info', 'Thank you for using Kimai ' ~ constant('App\\Constants::VERSION') ~ ' ' ~ constant('App\\Constants::STATUS') ~ ' - ' ~ constant('App\\Constants::NAME'), 'Kimai ' ~ constant('App\\Constants::VERSION'), 'fas fa-thumbs-up') }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="nav-tabs-custom">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#kimai" data-toggle="tab" aria-expanded="true">Kimai</a></li>
|
||||
<li><a href="#system" data-toggle="tab" aria-expanded="true">System</a></li>
|
||||
<li><a href="#license" data-toggle="tab" aria-expanded="true">License</a></li>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation"class ="active"><a href="#systeminfo" aria-controls="systeminfo" role="tab" data-toggle="tab">{{ 'tab.system'|trans({}, 'about') }}</a></li>
|
||||
<li role="presentation"><a href="#license" aria-controls="license" role="tab" data-toggle="tab">{{ 'tab.license'|trans({}, 'about') }}</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="kimai">
|
||||
<dl>
|
||||
<dt>Version</dt><dd>{{ constant('App\\Constants::VERSION') }}</dd>
|
||||
<dt>Status</dt><dd>{{ constant('App\\Constants::STATUS') }}</dd>
|
||||
<dt>Name</dt><dd>{{ constant('App\\Constants::NAME') }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="tab-pane" id="system">
|
||||
|
||||
<div role="tabpanel" class="tab-pane active" id="systeminfo">
|
||||
<div class="box-group" id="accordion">
|
||||
{% if is_granted('system_actions') %}
|
||||
<div class="panel box">
|
||||
<div class="box-header with-border">
|
||||
<h4 class="box-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseTwo" class="" aria-expanded="true">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseSystemAction" aria-expanded="true">
|
||||
{{ 'label.actions'|trans }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseSystemAction" class="panel-collapse collapse in" aria-expanded="true">
|
||||
<div class="box-body">
|
||||
<p>
|
||||
<a href="{{ path('system_flush_cache') }}" class="btn btn-primary" onclick="$(this).html('Clear cache <i class=\'fas fa-spinner fa-spin\'></i>')">Clear cache</a>
|
||||
</p>
|
||||
|
||||
{% if content_action is defined and content_action is not empty %}
|
||||
<pre>
|
||||
{{ content_action }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel box">
|
||||
<div class="box-header with-border">
|
||||
<h4 class="box-title">
|
||||
<a data-toggle="collapse" role="button" data-parent="#accordion" href="#collapseEnvironment" aria-expanded="false" aria-controls="collapseEnvironment">
|
||||
Environment
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseTwo" class="panel-collapse collapse in" aria-expanded="true" style="">
|
||||
<div id="collapseEnvironment" class="panel-collapse collapse" aria-expanded="false" role="tabpanel">
|
||||
<div class="box-body">
|
||||
|
||||
<dl>
|
||||
@@ -48,12 +67,12 @@
|
||||
<div class="panel box">
|
||||
<div class="box-header with-border">
|
||||
<h4 class="box-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseThree" class="collapsed" aria-expanded="false">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapsePhp" class="collapsed" aria-expanded="false">
|
||||
PHP
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseThree" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;">
|
||||
<div id="collapsePhp" class="panel-collapse collapse" aria-expanded="false">
|
||||
<div class="box-body">
|
||||
|
||||
<dl>
|
||||
@@ -77,12 +96,12 @@
|
||||
<div class="panel box">
|
||||
<div class="box-header with-border">
|
||||
<h4 class="box-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseFour" class="collapsed" aria-expanded="false">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapseSystem" class="collapsed" aria-expanded="false">
|
||||
Server
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseFour" class="panel-collapse collapse" aria-expanded="false" style="height: 0px;">
|
||||
<div id="collapseSystem" class="panel-collapse collapse" aria-expanded="false">
|
||||
<div class="box-body">
|
||||
|
||||
<dl>
|
||||
@@ -96,9 +115,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab-pane" id="license">
|
||||
<div role="tabpanel" class="tab-pane" id="license">
|
||||
<pre>{{ license }}</pre>
|
||||
<p>
|
||||
<a href="https://choosealicense.com/licenses/mit/" target="_blank">choosealicense.com</a>,
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_page_content_before %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_BEFORE')) %}
|
||||
{{ event.content|raw }}
|
||||
<div class="toolbar-pad no-print">
|
||||
{% block main_before %}{% endblock %}
|
||||
</div>
|
||||
@@ -17,10 +19,16 @@
|
||||
|
||||
{% block avanzu_page_content_after %}
|
||||
{% block main_after %}{% endblock %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_AFTER')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_page_content %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_START')) %}
|
||||
{{ event.content|raw }}
|
||||
{% block main %}{% endblock %}
|
||||
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_END')) %}
|
||||
{{ event.content|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block avanzu_document_title %}
|
||||
|
||||
@@ -222,3 +222,25 @@
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro plugins(view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'shop': {'url': constant('App\\Constants::HOMEPAGE') ~ '/store/', 'target': '_blank'}} %}
|
||||
|
||||
{% set event = trigger('actions.plugins', {'actions': actions, 'view': view}) %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro plugin(plugin, view) %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
|
||||
{% set actions = {'home': {'url': plugin.metadata.homepage, 'target': '_blank'}} %}
|
||||
|
||||
{% set event = trigger('actions.plugin', {'actions': actions, 'view': view}) %}
|
||||
{% if view == 'index' %}
|
||||
{{ widgets.button_group(event.payload.actions) }}
|
||||
{% else %}
|
||||
{{ widgets.page_actions(event.payload.actions) }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
{% set modal = null %}
|
||||
{% set toggle = null %}
|
||||
{% set url = null %}
|
||||
{% set target = null %}
|
||||
{% set class = "btn btn-default btn-" ~ icon ~ " " %}
|
||||
|
||||
{% if not values is iterable %}
|
||||
@@ -173,6 +174,7 @@
|
||||
{% set onclick = values.onclick ?? null %}
|
||||
{% set modal = values.modal ?? null %}
|
||||
{% set toggle = values.toggle ?? null %}
|
||||
{% set target = values.target ?? null %}
|
||||
{% set id = values.id ?? null %}
|
||||
{% set class = class ~ ( values.class | default("")) %}
|
||||
{% endif %}
|
||||
@@ -190,6 +192,9 @@
|
||||
{%- if onclick is not empty -%}
|
||||
onclick="{{ onclick }}"
|
||||
{%- endif -%}
|
||||
{%- if target is not empty -%}
|
||||
target="{{ target }}"
|
||||
{%- endif -%}
|
||||
>{{ macro.icon(icon) }}</a>
|
||||
{% endfor -%}
|
||||
</div>
|
||||
|
||||
58
templates/plugin/index.html.twig
Normal file
58
templates/plugin/index.html.twig
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% import "macros/actions.html.twig" as actions %}
|
||||
|
||||
{% block page_title %}{{ 'plugins.title'|trans({}, 'plugins') }}{% endblock %}
|
||||
{% block page_subtitle %}{{ 'plugins.subtitle'|trans({}, 'plugins') }}{% endblock %}
|
||||
{% block page_actions %}{{ actions.plugins('index') }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if plugins|length == 0 %}
|
||||
{{ widgets.callout('warning', 'plugin.none_installed'|trans({}, 'plugins')) }}
|
||||
{% else %}
|
||||
<div class="box data_table" id="datatable_plugins">
|
||||
<div class="box-body no-padding">
|
||||
<div class="dataTables_wrapper form-inline dt-bootstrap">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table class="table table-striped table-hover dataTable" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.name'|trans }}</th>
|
||||
<th>{{ 'label.version'|trans({}, 'plugins') }}</th>
|
||||
<th class="hidden-xs">{{ 'label.description'|trans }}</th>
|
||||
<th class="hidden-xs hidden-sm">{{ 'label.required_version'|trans({}, 'plugins') }}</th>
|
||||
<th>{{ 'label.actions'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for plugin in plugins %}
|
||||
<tr>
|
||||
<td>{{ plugin.name }}</td>
|
||||
<td>{{ widgets.label(plugin.metadata.version, 'primary') }}</td>
|
||||
<td class="hidden-xs">{{ plugin.metadata.description }}</td>
|
||||
<td class="hidden-xs hidden-sm">
|
||||
{% if plugin.metadata.kimaiVersion > constant('App\\Constants::VERSION') %}
|
||||
{{ widgets.label(plugin.metadata.kimaiVersion, 'danger') }}
|
||||
{% elseif plugin.metadata.kimaiVersion < constant('App\\Constants::VERSION') %}
|
||||
{{ widgets.label(plugin.metadata.kimaiVersion, 'warning') }}
|
||||
{% else %}
|
||||
{{ widgets.label(plugin.metadata.kimaiVersion, 'success') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ actions.plugin(plugin, 'index') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -31,10 +31,10 @@ class ConfigurationControllerTest extends APIControllerBaseTest
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(7, count($result));
|
||||
$this->assertStructure($result, false);
|
||||
$this->assertStructure($result);
|
||||
}
|
||||
|
||||
protected function assertStructure(array $result, $full = true)
|
||||
protected function assertStructure(array $result)
|
||||
{
|
||||
$expectedKeys = ['date', 'date_time', 'duration', 'form_date', 'form_date_time', 'is24hours', 'time'];
|
||||
$actual = array_keys($result);
|
||||
|
||||
@@ -29,9 +29,9 @@ class AboutControllerTest extends ControllerBaseTest
|
||||
$this->assertAccessIsGranted($client, '/admin/about');
|
||||
|
||||
$result = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav.nav-tabs li');
|
||||
$this->assertEquals(3, count($result));
|
||||
$this->assertEquals(2, count($result));
|
||||
|
||||
$result = $client->getCrawler()->filter('div.nav-tabs-custom div.tab-content div.tab-pane');
|
||||
$this->assertEquals(3, count($result));
|
||||
$this->assertEquals(2, count($result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class CalendarControllerTest extends ControllerBaseTest
|
||||
|
||||
$crawler = $client->getCrawler();
|
||||
$calendar = $crawler->filter('div#calendar');
|
||||
$this->assertEquals(1, $calendar->count());
|
||||
}
|
||||
|
||||
public function testCalendarEntriesAction()
|
||||
|
||||
@@ -202,6 +202,23 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
$this->assertEquals($count, $node->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
* @param array $buttons
|
||||
*/
|
||||
protected function assertPageActions(Client $client, array $buttons)
|
||||
{
|
||||
$node = $client->getCrawler()->filter('section.content-header div.breadcrumb div.box-tools div.btn-group a.btn');
|
||||
$this->assertEquals(count($buttons), $node->count());
|
||||
|
||||
foreach ($node->getIterator() as $element) {
|
||||
$expectedClass = str_replace('btn btn-default btn-', '', $element->getAttribute('class'));
|
||||
$this->assertArrayHasKey($expectedClass, $buttons);
|
||||
$expectedUrl = $buttons[$expectedClass];
|
||||
$this->assertEquals($expectedUrl, $element->getAttribute('href'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $role the USER role to use for the request
|
||||
* @param string $url the URL of the page displaying the initial form to submit
|
||||
|
||||
@@ -40,7 +40,6 @@ class ExportControllerTest extends ControllerBaseTest
|
||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||
|
||||
$begin = new \DateTime('first day of this month');
|
||||
$end = new \DateTime('last day of this month');
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture
|
||||
->setUser($this->getUserByRole($em, User::ROLE_USER))
|
||||
|
||||
48
tests/Controller/PluginControllerTest.php
Normal file
48
tests/Controller/PluginControllerTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Plugin\PluginManager;
|
||||
use App\Tests\Plugin\Fixtures\TestPlugin;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \App\Controller\PluginController
|
||||
* @group integration
|
||||
*/
|
||||
class PluginControllerTest extends ControllerBaseTest
|
||||
{
|
||||
public function testIsSecure()
|
||||
{
|
||||
$this->assertUrlIsSecured('/admin/plugins/');
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/plugins/');
|
||||
}
|
||||
|
||||
public function testIndexAction()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/plugins/');
|
||||
$this->assertCalloutWidgetWithMessage($client, 'You have no plugin installed yet');
|
||||
$this->assertPageActions($client, ['shop' => 'https://www.kimai.org/store/']);
|
||||
}
|
||||
|
||||
public function testIndexActionWithInstalledPlugins()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
/** @var PluginManager $manager */
|
||||
$manager = self::$container->get(PluginManager::class);
|
||||
$manager->addPlugin(new TestPlugin());
|
||||
|
||||
$this->assertAccessIsGranted($client, '/admin/plugins/');
|
||||
$this->assertHasDataTable($client);
|
||||
$this->assertDataTableRowCount($client, 'datatable_plugins', 1);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace App\Tests\Export\Renderer;
|
||||
|
||||
use App\Export\Renderer\HtmlRenderer;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Renderer\HtmlRenderer
|
||||
@@ -22,7 +22,7 @@ class HtmlRendererTest extends AbstractRendererTest
|
||||
public function testConfiguration()
|
||||
{
|
||||
$sut = new HtmlRenderer(
|
||||
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock()
|
||||
);
|
||||
|
||||
$this->assertEquals('html', $sut->getId());
|
||||
@@ -33,16 +33,13 @@ class HtmlRendererTest extends AbstractRendererTest
|
||||
public function testRender()
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
/** @var \Twig_Environment $twig */
|
||||
/** @var Environment $twig */
|
||||
$twig = $kernel->getContainer()->get('twig');
|
||||
$stack = $kernel->getContainer()->get('request_stack');
|
||||
$request = new Request();
|
||||
$request->setLocale('en');
|
||||
$stack->push($request);
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
|
||||
$sut = new HtmlRenderer($twig);
|
||||
|
||||
$response = $this->render($sut);
|
||||
|
||||
@@ -19,7 +19,7 @@ use App\Utils\MPdfConverter;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\Renderer\PDFRenderer
|
||||
@@ -45,7 +45,7 @@ class PdfRendererTest extends AbstractRendererTest
|
||||
public function testConfiguration()
|
||||
{
|
||||
$sut = new PDFRenderer(
|
||||
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock(),
|
||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(),
|
||||
$this->getDateTimeFactory(),
|
||||
$this->getMockBuilder(HtmlToPdfConverter::class)->getMock()
|
||||
);
|
||||
@@ -58,7 +58,7 @@ class PdfRendererTest extends AbstractRendererTest
|
||||
public function testRender()
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
/** @var \Twig_Environment $twig */
|
||||
/** @var Environment $twig */
|
||||
$twig = $kernel->getContainer()->get('twig');
|
||||
$stack = $kernel->getContainer()->get('request_stack');
|
||||
$cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
|
||||
@@ -67,9 +67,6 @@ class PdfRendererTest extends AbstractRendererTest
|
||||
$request->setLocale('en');
|
||||
$stack->push($request);
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
|
||||
$sut = new PDFRenderer($twig, $this->getDateTimeFactory(), $converter);
|
||||
|
||||
$response = $this->render($sut);
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace App\Tests\Export;
|
||||
use App\Export\Renderer\HtmlRenderer;
|
||||
use App\Export\ServiceExport;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Export\ServiceExport
|
||||
@@ -20,22 +21,22 @@ class ServiceExportTest extends TestCase
|
||||
{
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$sut = new ServiceExport();
|
||||
$sut = new ServiceExport([]);
|
||||
$this->assertEmpty($sut->getRenderer());
|
||||
}
|
||||
|
||||
public function testUnknownRendererReturnsNull()
|
||||
{
|
||||
$sut = new ServiceExport();
|
||||
$sut = new ServiceExport([]);
|
||||
$this->assertNull($sut->getRendererById('default'));
|
||||
}
|
||||
|
||||
public function testAdd()
|
||||
{
|
||||
$sut = new ServiceExport();
|
||||
$sut = new ServiceExport([]);
|
||||
|
||||
$sut->addRenderer(new HtmlRenderer(
|
||||
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock()
|
||||
));
|
||||
|
||||
$this->assertEquals(1, count($sut->getRenderer()));
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Tests\Invoice\Renderer;
|
||||
|
||||
use App\Invoice\Renderer\TwigRenderer;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
@@ -21,7 +22,7 @@ class TwigRendererTest extends AbstractRendererTest
|
||||
public function testSupports()
|
||||
{
|
||||
$loader = new FilesystemLoader();
|
||||
$env = new \Twig_Environment($loader);
|
||||
$env = new Environment($loader);
|
||||
$sut = new TwigRenderer($env);
|
||||
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('default.html.twig')));
|
||||
@@ -37,7 +38,7 @@ class TwigRendererTest extends AbstractRendererTest
|
||||
public function testRender()
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
/** @var \Twig_Environment $twig */
|
||||
/** @var Environment $twig */
|
||||
$twig = $kernel->getContainer()->get('twig');
|
||||
$stack = $kernel->getContainer()->get('request_stack');
|
||||
$request = new Request();
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Invoice\Renderer\TwigRenderer;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\ServiceInvoice
|
||||
@@ -65,7 +66,7 @@ class ServiceInvoiceTest extends TestCase
|
||||
$sut->addNumberGenerator(new DateNumberGenerator());
|
||||
$sut->addRenderer(
|
||||
new TwigRenderer(
|
||||
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||
$this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
31
tests/Plugin/Fixtures/TestPlugin.php
Normal file
31
tests/Plugin/Fixtures/TestPlugin.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Plugin\Fixtures;
|
||||
|
||||
use App\Plugin\PluginInterface;
|
||||
|
||||
class TestPlugin implements PluginInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'TestPlugin';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return __DIR__;
|
||||
}
|
||||
}
|
||||
29
tests/Plugin/Fixtures/composer.json
Normal file
29
tests/Plugin/Fixtures/composer.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "kimai/test-plugin-composer",
|
||||
"description": "Just a test fixture for the PluginManager",
|
||||
"homepage": "https://github.com/kevinpapst/kimai2",
|
||||
"type": "kimai-plugin",
|
||||
"require": {
|
||||
"kimai/kimai2-composer": "*",
|
||||
"kevinpapst/kimai2": "*"
|
||||
},
|
||||
"keywords": [
|
||||
"kimai",
|
||||
"kimai-plugin"
|
||||
],
|
||||
"license": "proprietary",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kevin Papst",
|
||||
"homepage": "https://www.kevinpapst.de"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"kimai": {
|
||||
"require": "0.9",
|
||||
"version": "1.0",
|
||||
"name": "TestPlugin from composer.json",
|
||||
"license": []
|
||||
}
|
||||
}
|
||||
}
|
||||
78
tests/Plugin/PluginManagerTest.php
Normal file
78
tests/Plugin/PluginManagerTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Plugin;
|
||||
|
||||
use App\Plugin\Plugin;
|
||||
use App\Plugin\PluginInterface;
|
||||
use App\Plugin\PluginManager;
|
||||
use App\Plugin\PluginMetadata;
|
||||
use App\Tests\Plugin\Fixtures\TestPlugin;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Plugin\PluginManager
|
||||
*/
|
||||
class PluginManagerTest extends TestCase
|
||||
{
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$sut = new PluginManager([]);
|
||||
$this->assertEmpty($sut->getPlugins());
|
||||
$this->assertNull($sut->getPlugin('foo'));
|
||||
}
|
||||
|
||||
public function testUnknownRendererReturnsNull()
|
||||
{
|
||||
$sut = new PluginManager([]);
|
||||
$this->assertNull($sut->getPlugin('foo'));
|
||||
}
|
||||
|
||||
public function testAdd()
|
||||
{
|
||||
$sut = new PluginManager([]);
|
||||
|
||||
$plugin = $this->getMockBuilder(PluginInterface::class)
|
||||
->setMethods(['getName', 'getPath'])
|
||||
->getMock();
|
||||
|
||||
$plugin->method('getName')->willReturn('foo');
|
||||
$plugin->method('getPath')->willReturn('bar');
|
||||
|
||||
$sut->addPlugin(new TestPlugin());
|
||||
$sut->addPlugin($plugin);
|
||||
$sut->addPlugin(new TestPlugin());
|
||||
|
||||
// make sure a plugin with the same name is not added twice, the first one wins!
|
||||
$this->assertEquals(2, count($sut->getPlugins()));
|
||||
|
||||
$foo = $sut->getPlugin('foo');
|
||||
$this->assertInstanceOf(Plugin::class, $foo);
|
||||
$this->assertEquals('foo', $foo->getName());
|
||||
$this->assertEquals('bar', $foo->getPath());
|
||||
|
||||
$test = $sut->getPlugin('TestPlugin');
|
||||
$this->assertInstanceOf(Plugin::class, $test);
|
||||
$this->assertEquals('TestPlugin', $test->getName());
|
||||
$this->assertEquals(new PluginMetadata(), $test->getMetadata());
|
||||
}
|
||||
|
||||
public function testLoadMetadata()
|
||||
{
|
||||
$sut = new PluginManager([new TestPlugin()]);
|
||||
$plugin = $sut->getPlugin('TestPlugin');
|
||||
$sut->loadMetadata($plugin);
|
||||
|
||||
$meta = $plugin->getMetadata();
|
||||
$this->assertEquals('0.9', $meta->getKimaiVersion());
|
||||
$this->assertEquals('1.0', $meta->getVersion());
|
||||
$this->assertEquals('Just a test fixture for the PluginManager', $meta->getDescription());
|
||||
$this->assertEquals('https://github.com/kevinpapst/kimai2', $meta->getHomepage());
|
||||
}
|
||||
}
|
||||
42
tests/Plugin/PluginMetadataTest.php
Normal file
42
tests/Plugin/PluginMetadataTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Plugin;
|
||||
|
||||
use App\Plugin\PluginMetadata;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Plugin\PluginMetadata
|
||||
*/
|
||||
class PluginMetadataTest extends TestCase
|
||||
{
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$sut = new PluginMetadata();
|
||||
$this->assertNull($sut->getDescription());
|
||||
$this->assertNull($sut->getHomepage());
|
||||
$this->assertNull($sut->getVersion());
|
||||
$this->assertNull($sut->getKimaiVersion());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter()
|
||||
{
|
||||
$sut = new PluginMetadata();
|
||||
$this->assertInstanceOf(PluginMetadata::class, $sut->setVersion('13.7'));
|
||||
$this->assertInstanceOf(PluginMetadata::class, $sut->setHomepage('http://www.example.com'));
|
||||
$this->assertInstanceOf(PluginMetadata::class, $sut->setDescription('foo bar'));
|
||||
$this->assertInstanceOf(PluginMetadata::class, $sut->setKimaiVersion('1.0'));
|
||||
|
||||
$this->assertEquals('13.7', $sut->getVersion());
|
||||
$this->assertEquals('http://www.example.com', $sut->getHomepage());
|
||||
$this->assertEquals('foo bar', $sut->getDescription());
|
||||
$this->assertEquals('1.0', $sut->getKimaiVersion());
|
||||
}
|
||||
}
|
||||
48
tests/Plugin/PluginTest.php
Normal file
48
tests/Plugin/PluginTest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Plugin;
|
||||
|
||||
use App\Plugin\Plugin;
|
||||
use App\Plugin\PluginMetadata;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Plugin\Plugin
|
||||
*/
|
||||
class PluginTest extends TestCase
|
||||
{
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$plugin = new Plugin();
|
||||
$this->assertNull($plugin->getName());
|
||||
$this->assertNull($plugin->getPath());
|
||||
$this->assertNull($plugin->getMetadata());
|
||||
}
|
||||
|
||||
public function testGetterAndSetter()
|
||||
{
|
||||
$metadata = new PluginMetadata();
|
||||
$metadata
|
||||
->setDescription('foo')
|
||||
->setHomepage('http://www.example.com')
|
||||
->setVersion('13.7')
|
||||
->setKimaiVersion('1.1')
|
||||
;
|
||||
|
||||
$plugin = new Plugin();
|
||||
$this->assertInstanceOf(Plugin::class, $plugin->setName('foo'));
|
||||
$this->assertInstanceOf(Plugin::class, $plugin->setPath('bar'));
|
||||
$this->assertInstanceOf(Plugin::class, $plugin->setMetadata($metadata));
|
||||
|
||||
$this->assertEquals('foo', $plugin->getName());
|
||||
$this->assertEquals('bar', $plugin->getPath());
|
||||
$this->assertSame($metadata, $plugin->getMetadata());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Intl\Util\IntlTestHelper;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* @covers \App\Twig\Extensions
|
||||
@@ -68,7 +69,7 @@ class ExtensionsTest extends TestCase
|
||||
$this->assertCount(count($functions), $twigFunctions);
|
||||
$i = 0;
|
||||
foreach ($twigFunctions as $filter) {
|
||||
$this->assertInstanceOf(\Twig_SimpleFunction::class, $filter);
|
||||
$this->assertInstanceOf(TwigFunction::class, $filter);
|
||||
$this->assertEquals($functions[$i++], $filter->getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
<source>about.subtitle</source>
|
||||
<target>System Informationen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab.system">
|
||||
<source>tab.system</source>
|
||||
<target>System Info</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab.license">
|
||||
<source>tab.license</source>
|
||||
<target>Kimai Lizenz</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
<source>about.subtitle</source>
|
||||
<target>System information</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab.system">
|
||||
<source>tab.system</source>
|
||||
<target>System Info</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="tab.license">
|
||||
<source>tab.license</source>
|
||||
<target>Kimai License</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
<source>menu.admin</source>
|
||||
<target>Administration</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.system">
|
||||
<source>menu.system</source>
|
||||
<target>System</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.logout">
|
||||
<source>menu.logout</source>
|
||||
<target>Abmelden</target>
|
||||
@@ -121,6 +125,10 @@
|
||||
<source>menu.admin_user</source>
|
||||
<target>Benutzer</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.plugin">
|
||||
<source>menu.plugin</source>
|
||||
<target>Erweiterungen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.system_configuration">
|
||||
<source>menu.system_configuration</source>
|
||||
<target>Settings</target>
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
<source>menu.admin</source>
|
||||
<target>Administration</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.system">
|
||||
<source>menu.system</source>
|
||||
<target>System</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.logout">
|
||||
<source>menu.logout</source>
|
||||
<target>Logout</target>
|
||||
@@ -121,6 +125,10 @@
|
||||
<source>menu.admin_user</source>
|
||||
<target>User</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.plugin">
|
||||
<source>menu.plugin</source>
|
||||
<target>Plugins</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="menu.system_configuration">
|
||||
<source>menu.system_configuration</source>
|
||||
<target>System configuration</target>
|
||||
|
||||
31
translations/plugins.de.xliff
Normal file
31
translations/plugins.de.xliff
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file date="2018-10-29T10:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="plugins.en.xliff">
|
||||
<body>
|
||||
<trans-unit id="plugins.title">
|
||||
<source>plugins.title</source>
|
||||
<target>Plugins</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugins.subtitle">
|
||||
<source>plugins.subtitle</source>
|
||||
<target>Mit Plugins können Sie den Funktionsumfang von Kimai erweitern</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.version">
|
||||
<source>label.version</source>
|
||||
<target>Version</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.required_version">
|
||||
<source>label.required_version</source>
|
||||
<target>Kompatibel mit</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugin.none_installed">
|
||||
<source>plugin.none_installed</source>
|
||||
<target>Sie haben noch kein Plugin installiert.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugin.marketplace">
|
||||
<source>plugin.marketplace</source>
|
||||
<target>Kimai Marketplace</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
31
translations/plugins.en.xliff
Normal file
31
translations/plugins.en.xliff
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file date="2018-10-29T10:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
||||
<body>
|
||||
<trans-unit id="plugins.title">
|
||||
<source>plugins.title</source>
|
||||
<target>Plugins</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugins.subtitle">
|
||||
<source>plugins.subtitle</source>
|
||||
<target>You can enhance the functionality of Kimai with plugins</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.version">
|
||||
<source>label.version</source>
|
||||
<target>Version</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.required_version">
|
||||
<source>label.required_version</source>
|
||||
<target>Compatible with</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugin.none_installed">
|
||||
<source>plugin.none_installed</source>
|
||||
<target>You have no plugin installed yet.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="plugin.marketplace">
|
||||
<source>plugin.marketplace</source>
|
||||
<target>Kimai marketplace</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -23,15 +23,13 @@ Encore
|
||||
// generate only two files: app.js and app.css
|
||||
.addEntry('app', './assets/app.js')
|
||||
|
||||
// enable sass/scss parser
|
||||
.enableSassLoader()
|
||||
|
||||
// show OS notifications when builds finish/fail
|
||||
.enableBuildNotifications()
|
||||
|
||||
// load jquery as Kimai and AdminLTE rely on it
|
||||
.autoProvidejQuery()
|
||||
|
||||
// enable sass/scss parser
|
||||
// see https://symfony.com/doc/current/frontend/encore/bootstrap.html
|
||||
.enableSassLoader(function(sassOptions) {}, {
|
||||
resolveUrlLoader: false
|
||||
|
||||
Reference in New Issue
Block a user