Plugins support (#592)

This commit is contained in:
Kevin Papst
2019-02-24 15:54:38 +01:00
committed by GitHub
parent 36a158a805
commit 03aa4c9ddc
11 changed files with 220 additions and 35 deletions

7
.gitignore vendored
View File

@@ -8,8 +8,8 @@
templates/invoice/renderer/.~lock* templates/invoice/renderer/.~lock*
/var/data/*.sqlite /var/data/*
/var/data/*.sqlite-journal !/var/data/.gitkeep
!/var/data/kimai_test.sqlite !/var/data/kimai_test.sqlite
/var/coverage/ /var/coverage/
/var/cache/* /var/cache/*
@@ -17,6 +17,9 @@ templates/invoice/renderer/.~lock*
/var/invoices/* /var/invoices/*
/var/plugins/*
!var/plugins/.gitkeep
/var/log/* /var/log/*
!var/log/.gitkeep !var/log/.gitkeep

View File

@@ -1,6 +1,6 @@
# Kimai 2 - Time-tracking made easy # Kimai 2 - Time-tracking made easy
Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](http://v2.kimai.org)). Kimai - the open source time-tracker application with a mobile-first approach (read more at the [official website](https://www.kimai.org)).
[![Latest Stable Version](https://poser.pugx.org/kevinpapst/kimai2/v/stable)](https://packagist.org/packages/kevinpapst/kimai2) [![Latest Stable Version](https://poser.pugx.org/kevinpapst/kimai2/v/stable)](https://packagist.org/packages/kevinpapst/kimai2)
[![License](https://poser.pugx.org/kevinpapst/kimai2/license)](https://packagist.org/packages/kevinpapst/kimai2) [![License](https://poser.pugx.org/kevinpapst/kimai2/license)](https://packagist.org/packages/kevinpapst/kimai2)
@@ -30,53 +30,51 @@ Kimai is a [multi-language application](var/docs/translations.md) and already tr
## Documentation ## Documentation
Looking for more information about Kimai 2? Check out our detailed [documentation](var/docs/). Looking for more information about Kimai 2? Check out our detailed [documentation](https://www.kimai.org/documentation/).
### Installation ### Installation
There are multiple ways to install Kimai, all of them described in the [installation docu](var/docs/installation.md): There are multiple ways to install Kimai, all of them described in the [installation docu](https://www.kimai.org/documentation/installation.html):
- [Recommended installation with GIT and Composer](var/docs/installation.md#recommended-setup) - [Recommended installation with GIT and Composer](https://www.kimai.org/documentation/installation.html#recommended-setup)
- [Development setup](var/docs/installation.md#development-installation) - [Development setup](https://www.kimai.org/documentation/installation.html#development-installation)
- [Docker](var/docs/docker.md) - [Docker](https://www.kimai.org/documentation/docker.html)
- [1-click installations](var/docs/installation.md#hosting-and-1-click-installations) - [1-click installations](https://www.kimai.org/documentation/installation.html#hosting-and-1-click-installations)
- [FTP](var/docs/installation.md#ftp-installation) - [FTP](https://www.kimai.org/documentation/installation.html#ftp-installation)
### Updating Kimai ### Updating Kimai
Read the following documentations before you start your upgrade: Read the following documentations before you start your upgrade:
- The [update documentation](var/docs/updates.md) - The [update documentation](https://www.kimai.org/documentation/updates.html)
- The version specific [UPGRADING guide](UPGRADING.md) - The version specific [UPGRADING guide](UPGRADING.md)
- The [release information](https://github.com/kevinpapst/kimai2/releases) - The [release information](https://github.com/kevinpapst/kimai2/releases)
## Roadmap and releases ## Roadmap and releases
You can see our development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections. You can see a rough development roadmap in the [Milestones](https://github.com/kevinpapst/kimai2/milestones) sections.
It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome! It is open for changes and input from the community, your [ideas and questions](https://github.com/kevinpapst/kimai2/issues) are welcome!
> Kimai 2 uses a rolling release concept for delivering updates. > Kimai 2 uses a rolling release concept for delivering updates.
> You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first! > You can upgrade Kimai at any time, you don't need to wait for the next official release. Read the [upgrade docs](UPGRADING.md) first!
Release versions will be created on a regular base and you can use these tags if you are familiar with Git, Release versions will be created on a regular base (approx. 1 per month) and you can use these tags if you are familiar with Git,
but we will not provide support for any specific version. but we cannot provide support for any specific version (especially older ones).
Every code change, whether it's a new feature or a bug fix, will be done on the master branch. Every code change, whether it's a new feature or a bug fix, will be done on the master branch.
I have to do it this way, as I develop Kimai in my free time and want to put most I have to do it this way, as I develop Kimai in my free time and want to put most
effort into the software instead of backporting changes for old versions. effort into the software instead of backporting changes for old versions.
## Extensions for Kimai 2 ## Plugins for Kimai 2
As Kimai 2 was built on top of Symfony, it can be extended like every other Symfony application. Kimai 2 was built on top of the Symfony framework and can be extended in various ways:
We call these extensions bundles, but you might also know them as add-ons, extensions or plugins.
All available Kimai 2 bundles can be found at the [Kimai recipes](https://github.com/kimai/recipes) repository. - Users looking for existing plugins go to our [plugin marketplace](https://www.kimai.org/store/)
- Developer start with the [developer documentation](https://www.kimai.org/documentation/developers.html)
## Developer ## Credits
Kimai 2 is developed with modern frameworks like [Symfony v4](https://github.com/symfony/symfony), [Doctrine](https://github.com/doctrine/), Kimai 2 is developed with modern frameworks like
[AdminLTE](https://github.com/kevinpapst/AdminLTEBundle/) and [many](composer.json) [more](package.json). [Symfony v4](https://github.com/symfony/symfony),
[Doctrine](https://github.com/doctrine/),
If you want to start developing for Kimai 2, please read the following documentation: [AdminLTEBundle](https://github.com/kevinpapst/AdminLTEBundle/) (based on [AdminLTE theme](https://github.com/almasaeed2010/AdminLTE)) and
[many](composer.json) [more](package.json).
- an example on how to extend Kimai 2 can be found in this [GitHub repository](https://github.com/kevinpapst/kimai2-invoice)
- the [developer documentation](var/docs/developers.md) is available both on GitHub and your local installation

View File

@@ -68,7 +68,8 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"App\\": "src/" "App\\": "src/",
"KimaiPlugin\\": "var/plugins/"
} }
}, },
"autoload-dev": { "autoload-dev": {

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the AdminLTE bundle.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Controller;
use App\Event\ThemeEvent;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class EventController extends Controller
{
/**
* @var EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->eventDispatcher = $dispatcher;
}
/**
* @return EventDispatcherInterface
*/
protected function getDispatcher()
{
return $this->eventDispatcher;
}
/**
* @param string $eventName
*
* @return bool
*/
protected function hasListener($eventName)
{
return $this->getDispatcher()->hasListeners($eventName);
}
/**
* @param Request $request
* @param string $eventName
* @return ThemeEvent|Response
*/
public function trigger(Request $request, string $event)
{
if (!$this->hasListener($event)) {
return new Response();
}
$themeEvent = new ThemeEvent($request, $this->getUser());
$this->getDispatcher()->dispatch($event, $themeEvent);
return new Response($themeEvent->getContent());
}
}

View File

@@ -32,10 +32,10 @@ class CustomerFixtures extends Fixture
public const MAX_BUDGET = 100000; public const MAX_BUDGET = 100000;
public const MIN_GLOBAL_ACTIVITIES = 5; public const MIN_GLOBAL_ACTIVITIES = 5;
public const MAX_GLOBAL_ACTIVITIES = 50; public const MAX_GLOBAL_ACTIVITIES = 50;
public const MIN_PROJECTS_PER_CUSTOMER = 10; public const MIN_PROJECTS_PER_CUSTOMER = 1;
public const MAX_PROJECTS_PER_CUSTOMER = 50; public const MAX_PROJECTS_PER_CUSTOMER = 25;
public const MIN_ACTIVITIES_PER_PROJECT = 0; public const MIN_ACTIVITIES_PER_PROJECT = 0;
public const MAX_ACTIVITIES_PER_PROJECT = 15; public const MAX_ACTIVITIES_PER_PROJECT = 25;
/** /**
* @param ObjectManager $manager * @param ObjectManager $manager
@@ -46,18 +46,18 @@ class CustomerFixtures extends Fixture
$amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS); $amountCustomers = rand(self::MIN_CUSTOMERS, self::MAX_CUSTOMERS);
for ($c = 1; $c <= $amountCustomers; $c++) { for ($c = 1; $c <= $amountCustomers; $c++) {
$visibleCustomer = 0 != $c % 3; $visibleCustomer = 0 != $c % 5;
$customer = $this->createCustomer($faker, $visibleCustomer); $customer = $this->createCustomer($faker, $visibleCustomer);
$projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER); $projectForCustomer = rand(self::MIN_PROJECTS_PER_CUSTOMER, self::MAX_PROJECTS_PER_CUSTOMER);
for ($p = 1; $p <= $projectForCustomer; $p++) { for ($p = 1; $p <= $projectForCustomer; $p++) {
$visibleProject = 0 != $p % 3; $visibleProject = 0 != $p % 7;
$project = $this->createProject($faker, $customer, $visibleProject); $project = $this->createProject($faker, $customer, $visibleProject);
$manager->persist($project); $manager->persist($project);
$activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT); $activityForProject = rand(self::MIN_ACTIVITIES_PER_PROJECT, self::MAX_ACTIVITIES_PER_PROJECT);
for ($a = 1; $a <= $activityForProject; $a++) { for ($a = 1; $a <= $activityForProject; $a++) {
$visibleActivity = 0 != $a % 3; $visibleActivity = 0 != $a % 6;
$activity = $this->createActivity($faker, $project, $visibleActivity); $activity = $this->createActivity($faker, $project, $visibleActivity);
$manager->persist($activity); $manager->persist($activity);
} }

View File

@@ -33,6 +33,10 @@ class AppExtension extends Extension implements PrependExtensionInterface
$config = []; $config = [];
} }
// safe alternatives to %kernel.project_dir%
$container->setParameter('kimai.data_dir', $container->getParameter('kernel.project_dir') . '/var/data');
$container->setParameter('kimai.plugin_dir', $container->getParameter('kernel.project_dir') . '/var/plugins');
$container->setParameter('kimai.languages', $config['languages']); $container->setParameter('kimai.languages', $config['languages']);
$container->setParameter('kimai.calendar', $config['calendar']); $container->setParameter('kimai.calendar', $config['calendar']);
$container->setParameter('kimai.dashboard', $config['dashboard']); $container->setParameter('kimai.dashboard', $config['dashboard']);

78
src/Event/ThemeEvent.php Normal file
View 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\Event;
use App\Entity\User;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;
class ThemeEvent extends Event
{
public const JAVASCRIPT = 'app.theme.javascript';
public const STYLESHEET = 'app.theme.css';
public const HTML_HEAD = 'app.theme.html_head';
/**
* @var User
*/
protected $user;
/**
* @var Request
*/
protected $request;
/**
* @var string
*/
protected $content = '';
/**
* @param Request $request
* @param User $user
*/
public function __construct(Request $request, User $user)
{
$this->request = $request;
$this->user = $user;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @return Request
*/
public function getRequest(): Request
{
return $this->request;
}
/**
* @return string
*/
public function getContent(): string
{
return $this->content;
}
/**
* @param string $content
* @return ThemeEvent
*/
public function addContent(string $content)
{
$this->content .= $content;
return $this;
}
}

View File

@@ -23,6 +23,8 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\Kernel as BaseKernel; use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder; use Symfony\Component\Routing\RouteCollectionBuilder;
@@ -64,6 +66,30 @@ class Kernel extends BaseKernel
yield new $class(); yield new $class();
} }
} }
$pluginsDir = $this->getProjectDir() . '/var/plugins';
if (!file_exists($pluginsDir)) {
return;
}
$finder = new Finder();
$finder->ignoreUnreadableDirs()->directories()->name('*Bundle');
/** @var SplFileInfo $bundleDir */
foreach ($finder->in($pluginsDir) as $bundleDir) {
$bundleName = $bundleDir->getRelativePathname();
$bundleFilename = $bundleDir->getRealPath() . '/' . $bundleName . '.php';
if (!file_exists($bundleFilename)) {
continue;
}
$pluginClass = 'KimaiPlugin\\' . $bundleName . '\\' . $bundleName;
if (!class_exists($pluginClass)) {
continue;
}
yield new $pluginClass;
}
} }
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
@@ -106,6 +132,10 @@ class Kernel extends BaseKernel
// load application routes // load application routes
$routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob'); $routes->import($confDir . '/routes' . self::CONFIG_EXTS, '/', 'glob');
// load plugin routes
$pluginsDir = $this->getProjectDir() . '/var/plugins';
$routes->import($pluginsDir . '/*Bundle/Resources/config/routes'. self::CONFIG_EXTS, '/', 'glob');
} }
protected function configureFosUserRoutes(RouteCollectionBuilder $routes) protected function configureFosUserRoutes(RouteCollectionBuilder $routes)

View File

@@ -42,14 +42,14 @@
{% if entry.project and entry.project.customer %} {% if entry.project and entry.project.customer %}
<a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a> <a href="{{ path('admin_customer_edit', {'id' : entry.project.customer.id}) }}">{{ widgets.label_customer(entry.project.customer) }}</a>
{% else %} {% else %}
{#{ widgets.label('x', 'success') }#} {# GLOBAL ACTIVTITY DOES NOT HAVE A CUSTOMER #}
{% endif %} {% endif %}
</td> </td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}"> <td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
{% if entry.project %} {% if entry.project %}
<a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a> <a href="{{ path('admin_project_edit', {'id' : entry.project.id}) }}">{{ widgets.label_project(entry.project) }}</a>
{% else %} {% else %}
{#{ widgets.label('x', 'success') }#} {# GLOBAL ACTIVTITY DOES NOT HAVE A PROJECT #}
{% endif %} {% endif %}
</td> </td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td> <td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>

View File

@@ -89,12 +89,14 @@
{% block stylesheets %} {% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #} {# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}
<link rel="stylesheet" href="{{ asset('build/app.css') }}"> <link rel="stylesheet" href="{{ asset('build/app.css') }}">
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::STYLESHEET')})) }}
{% endblock %} {% endblock %}
{% block head %} {% block head %}
{{ parent() }} {{ parent() }}
<script src="{{ asset('build/app.js') }}"></script> <script src="{{ asset('build/app.js') }}"></script>
{% include 'partials/head.html.twig' %} {% include 'partials/head.html.twig' %}
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::HTML_HEAD')})) }}
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
@@ -119,4 +121,5 @@
{# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #} {# $.kimai.pauseRecord('li.messages-menu ul.menu li'); #}
}); });
</script> </script>
{{ render(controller('App\\Controller\\EventController::trigger', {'request':app.request, 'event':constant('App\\Event\\ThemeEvent::JAVASCRIPT')})) }}
{% endblock %} {% endblock %}

1
var/plugins/.gitkeep Normal file
View File

@@ -0,0 +1 @@
This directory can store as Kimai plugin/bundle folder.