added database driven system configurations with admin screen (#647)
This commit is contained in:
@@ -30,7 +30,6 @@ The table prefix is now hardcoded to `kimai2_`. If you used another prefix, you
|
|||||||
before starting the update process. You can also delete the row `DATABASE_PREFIX` from your `.env` file.
|
before starting the update process. You can also 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)
|
- API: DateTime objects will be returned including timezone identifier (previously 2019-03-02 14:23 - now 2019-03-02T14:23:00+00:00)
|
||||||
|
|
||||||
|
|
||||||
## [0.8.1](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
|
## [0.8.1](https://github.com/kevinpapst/kimai2/releases/tag/0.8.1)
|
||||||
|
|
||||||
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
A bug fixing release. Remember to execute the necessary timezone conversion script, if you haven't updated to 0.8 before (see below)!
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ kimai:
|
|||||||
|
|
||||||
# If you want to limit the max. active entries per user, you can do it here.
|
# If you want to limit the max. active entries per user, you can do it here.
|
||||||
#
|
#
|
||||||
# The soft_limit is used as theme setting (formerly "kimai.theme.active_warning"):
|
# The soft_limit is used as theme setting and displays a warning color if the user has at least X active recordings
|
||||||
# display a warning color if the user has at least X active recordings
|
|
||||||
#
|
#
|
||||||
# The hard_limit is used to detect how many active records are allowed per user:
|
# The hard_limit is used to detect how many active records are allowed per user:
|
||||||
# By default a user can only have one active time-record. The first one is stopped when the new one starts.
|
# By default a user can only have one active time-record. The first one is stopped when the new one starts.
|
||||||
@@ -98,7 +97,7 @@ kimai:
|
|||||||
ROLE_USER: []
|
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_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_ADMIN: [hourly-rate_own_profile]
|
||||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information]
|
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_configuration]
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Language specific settings, like the date formats
|
# Language specific settings, like the date formats
|
||||||
@@ -266,9 +265,7 @@ kimai:
|
|||||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||||
|
|
||||||
# Default settings used to populate forms
|
# --------------------------------------------------------------------------------
|
||||||
#defaults:
|
# Storage directories
|
||||||
# customer:
|
data_dir: '%kernel.project_dir%/var/data'
|
||||||
# timezone: Europe/Berlin
|
plugin_dir: '%kernel.project_dir%/var/plugins'
|
||||||
# country: DE
|
|
||||||
# currency: EUR
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ services:
|
|||||||
# The best practice is to be explicit about your dependencies anyway.
|
# The best practice is to be explicit about your dependencies anyway.
|
||||||
bind:
|
bind:
|
||||||
$projectDirectory: "%kernel.project_dir%"
|
$projectDirectory: "%kernel.project_dir%"
|
||||||
$hardLimit: "%kimai.timesheet.active_entries.hard_limit%"
|
|
||||||
|
|
||||||
# makes classes in src/ available to be used as services
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
@@ -47,17 +46,20 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$dashboard: "%kimai.dashboard%"
|
$dashboard: "%kimai.dashboard%"
|
||||||
|
|
||||||
App\Twig\MarkdownExtension:
|
|
||||||
arguments:
|
|
||||||
$timesheetAsMarkdown: "%kimai.timesheet.markdown%"
|
|
||||||
|
|
||||||
App\Controller\CustomerController:
|
|
||||||
arguments:
|
|
||||||
$defaults: "%kimai.defaults%"
|
|
||||||
|
|
||||||
App\Configuration\LanguageFormattings:
|
App\Configuration\LanguageFormattings:
|
||||||
|
arguments: ["%kimai.languages%"]
|
||||||
|
|
||||||
|
App\Configuration\TimesheetConfiguration:
|
||||||
arguments:
|
arguments:
|
||||||
$languageSettings: "%kimai.languages%"
|
$settings: "%kimai.timesheet%"
|
||||||
|
|
||||||
|
App\Configuration\FormConfiguration:
|
||||||
|
arguments:
|
||||||
|
$settings: "%kimai.defaults%"
|
||||||
|
|
||||||
|
App\Configuration\SystemConfiguration:
|
||||||
|
arguments:
|
||||||
|
$settings: "%kimai.config%"
|
||||||
|
|
||||||
App\Utils\MPdfConverter:
|
App\Utils\MPdfConverter:
|
||||||
arguments: ['%kernel.cache_dir%']
|
arguments: ['%kernel.cache_dir%']
|
||||||
@@ -104,19 +106,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||||
|
|
||||||
App\Form\TimesheetEditForm:
|
|
||||||
arguments:
|
|
||||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
|
||||||
|
|
||||||
App\Form\Extension\SelectWithApiDataExtension:
|
App\Form\Extension\SelectWithApiDataExtension:
|
||||||
tags:
|
tags:
|
||||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||||
|
|
||||||
App\Validator\Constraints\TimesheetValidator:
|
|
||||||
arguments:
|
|
||||||
$ruleset: "%kimai.timesheet.rules%"
|
|
||||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# THEME
|
# THEME
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
@@ -175,6 +168,11 @@ services:
|
|||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\InvoiceTemplate']
|
arguments: ['App\Entity\InvoiceTemplate']
|
||||||
|
|
||||||
|
App\Repository\ConfigurationRepository:
|
||||||
|
class: Doctrine\ORM\EntityRepository
|
||||||
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
|
arguments: ['App\Entity\Configuration']
|
||||||
|
|
||||||
App\Repository\InvoiceDocumentRepository:
|
App\Repository\InvoiceDocumentRepository:
|
||||||
class: App\Repository\InvoiceDocumentRepository
|
class: App\Repository\InvoiceDocumentRepository
|
||||||
arguments: ['%kimai.invoice.documents%']
|
arguments: ['%kimai.invoice.documents%']
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ services:
|
|||||||
public: true
|
public: true
|
||||||
|
|
||||||
test.PasswordEncoder: "@security.encoder_factory"
|
test.PasswordEncoder: "@security.encoder_factory"
|
||||||
|
|
||||||
|
App\Configuration\SystemConfiguration:
|
||||||
|
arguments: ['@App\Repository\ConfigurationRepository', "%kimai.config%"]
|
||||||
|
|
||||||
|
App\Repository\ConfigurationRepository:
|
||||||
|
class: Doctrine\ORM\EntityRepository
|
||||||
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
|
arguments: ['App\Entity\Configuration']
|
||||||
|
|||||||
12242
public/build/app.css
12242
public/build/app.css
File diff suppressed because one or more lines are too long
52257
public/build/app.js
52257
public/build/app.js
File diff suppressed because one or more lines are too long
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"build/app.js": "./app.js?3589af990f22f7616e76",
|
"build/app.js": "./app.js?fe004f53e686704f95d7",
|
||||||
"build/app.css": "./app.css?d19ec32eb7c1a1b17c99cc2154dd69dc",
|
"build/app.css": "./app.css?d1c5aa3942706f7bffb181e234e112de",
|
||||||
"build/images/blue.png": "./images/blue.png?96f8a905",
|
|
||||||
"build/images/blue@2x.png": "./images/blue@2x.png?2694acfd",
|
"build/images/blue@2x.png": "./images/blue@2x.png?2694acfd",
|
||||||
|
"build/images/blue.png": "./images/blue.png?96f8a905",
|
||||||
|
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
|
||||||
|
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",
|
||||||
|
"build/images/glyphicons-halflings-regular.svg": "./images/glyphicons-halflings-regular.svg?89889688",
|
||||||
|
"build/fonts/glyphicons-halflings-regular.eot": "./fonts/glyphicons-halflings-regular.eot?f4769f9b",
|
||||||
|
"build/fonts/fa-solid-900.ttf": "./fonts/fa-solid-900.ttf?a7a790d4",
|
||||||
"build/fonts/fa-brands-400.eot": "./fonts/fa-brands-400.eot?69b310ed",
|
"build/fonts/fa-brands-400.eot": "./fonts/fa-brands-400.eot?69b310ed",
|
||||||
"build/images/fa-brands-400.svg": "./images/fa-brands-400.svg?5da7e33c",
|
|
||||||
"build/fonts/fa-brands-400.ttf": "./fonts/fa-brands-400.ttf?d94572dd",
|
|
||||||
"build/fonts/fa-brands-400.woff": "./fonts/fa-brands-400.woff?e9f83339",
|
|
||||||
"build/fonts/fa-brands-400.woff2": "./fonts/fa-brands-400.woff2?66f625f1",
|
|
||||||
"build/fonts/fa-regular-400.eot": "./fonts/fa-regular-400.eot?0b7928f6",
|
"build/fonts/fa-regular-400.eot": "./fonts/fa-regular-400.eot?0b7928f6",
|
||||||
"build/images/fa-regular-400.svg": "./images/fa-regular-400.svg?4e41caaf",
|
"build/fonts/fa-brands-400.woff": "./fonts/fa-brands-400.woff?e9f83339",
|
||||||
"build/fonts/fa-regular-400.ttf": "./fonts/fa-regular-400.ttf?cdd8fa76",
|
"build/fonts/glyphicons-halflings-regular.woff2": "./fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
||||||
|
"build/fonts/fa-solid-900.eot": "./fonts/fa-solid-900.eot?0b93480e",
|
||||||
|
"build/fonts/fa-solid-900.woff": "./fonts/fa-solid-900.woff?dfc040d5",
|
||||||
"build/fonts/fa-regular-400.woff": "./fonts/fa-regular-400.woff?d9e29124",
|
"build/fonts/fa-regular-400.woff": "./fonts/fa-regular-400.woff?d9e29124",
|
||||||
"build/fonts/fa-regular-400.woff2": "./fonts/fa-regular-400.woff2?930c1264",
|
"build/fonts/fa-regular-400.woff2": "./fonts/fa-regular-400.woff2?930c1264",
|
||||||
"build/fonts/fa-solid-900.eot": "./fonts/fa-solid-900.eot?0b93480e",
|
"build/images/fa-regular-400.svg": "./images/fa-regular-400.svg?4e41caaf",
|
||||||
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",
|
"build/fonts/fa-regular-400.ttf": "./fonts/fa-regular-400.ttf?cdd8fa76",
|
||||||
"build/fonts/fa-solid-900.ttf": "./fonts/fa-solid-900.ttf?a7a790d4",
|
"build/fonts/fa-brands-400.ttf": "./fonts/fa-brands-400.ttf?d94572dd",
|
||||||
"build/fonts/fa-solid-900.woff": "./fonts/fa-solid-900.woff?dfc040d5",
|
|
||||||
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
|
|
||||||
"build/fonts/glyphicons-halflings-regular.eot": "./fonts/glyphicons-halflings-regular.eot?f4769f9b",
|
|
||||||
"build/images/glyphicons-halflings-regular.svg": "./images/glyphicons-halflings-regular.svg?89889688",
|
|
||||||
"build/fonts/glyphicons-halflings-regular.ttf": "./fonts/glyphicons-halflings-regular.ttf?e18bbf61",
|
|
||||||
"build/fonts/glyphicons-halflings-regular.woff": "./fonts/glyphicons-halflings-regular.woff?fa277232",
|
"build/fonts/glyphicons-halflings-regular.woff": "./fonts/glyphicons-halflings-regular.woff?fa277232",
|
||||||
"build/fonts/glyphicons-halflings-regular.woff2": "./fonts/glyphicons-halflings-regular.woff2?448c34a5",
|
"build/fonts/fa-brands-400.woff2": "./fonts/fa-brands-400.woff2?66f625f1",
|
||||||
|
"build/fonts/glyphicons-halflings-regular.ttf": "./fonts/glyphicons-halflings-regular.ttf?e18bbf61",
|
||||||
|
"build/images/fa-brands-400.svg": "./images/fa-brands-400.svg?5da7e33c",
|
||||||
"build/images/default_avatar.png": "./images/default_avatar.png?fc197350",
|
"build/images/default_avatar.png": "./images/default_avatar.png?fc197350",
|
||||||
"build/images/signature.png": "./images/signature.png?5f58501e"
|
"build/images/signature.png": "./images/signature.png?5f58501e"
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\API;
|
namespace App\API;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Form\TimesheetEditForm;
|
use App\Form\TimesheetEditForm;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
@@ -37,16 +38,14 @@ class TimesheetController extends BaseApiController
|
|||||||
* @var TimesheetRepository
|
* @var TimesheetRepository
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ViewHandlerInterface
|
* @var ViewHandlerInterface
|
||||||
*/
|
*/
|
||||||
protected $viewHandler;
|
protected $viewHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var TimesheetConfiguration
|
||||||
*/
|
*/
|
||||||
protected $hardLimit;
|
protected $configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
@@ -57,13 +56,13 @@ class TimesheetController extends BaseApiController
|
|||||||
* @param ViewHandlerInterface $viewHandler
|
* @param ViewHandlerInterface $viewHandler
|
||||||
* @param TimesheetRepository $repository
|
* @param TimesheetRepository $repository
|
||||||
* @param UserDateTimeFactory $dateTime
|
* @param UserDateTimeFactory $dateTime
|
||||||
* @param int $hardLimit
|
* @param TimesheetConfiguration $configuration
|
||||||
*/
|
*/
|
||||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, int $hardLimit)
|
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||||
{
|
{
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
$this->hardLimit = $hardLimit;
|
$this->configuration = $configuration;
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +234,7 @@ class TimesheetController extends BaseApiController
|
|||||||
if (null === $timesheet->getEnd()) {
|
if (null === $timesheet->getEnd()) {
|
||||||
$this->repository->stopActiveEntries(
|
$this->repository->stopActiveEntries(
|
||||||
$timesheet->getUser(),
|
$timesheet->getUser(),
|
||||||
$this->hardLimit
|
$this->configuration->getActiveEntriesHardLimit()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||||||
*/
|
*/
|
||||||
class CreateReleaseCommand extends Command
|
class CreateReleaseCommand extends Command
|
||||||
{
|
{
|
||||||
const CLONE_CMD = 'git clone -b %s --depth 1 https://github.com/kevinpapst/kimai2.git';
|
public const CLONE_CMD = 'git clone -b %s --depth 1 https://github.com/kevinpapst/kimai2.git';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
@@ -62,6 +62,7 @@ class CreateReleaseCommand extends Command
|
|||||||
|
|
||||||
if (getenv('APP_ENV') === 'prod') {
|
if (getenv('APP_ENV') === 'prod') {
|
||||||
$io->error('kimai:create-release is not allowed in production');
|
$io->error('kimai:create-release is not allowed in production');
|
||||||
|
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +78,13 @@ class CreateReleaseCommand extends Command
|
|||||||
|
|
||||||
if (!is_dir($directory)) {
|
if (!is_dir($directory)) {
|
||||||
$io->error('Given directory is not existing: ' . $directory);
|
$io->error('Given directory is not existing: ' . $directory);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_dir($directory) && !is_writable($directory)) {
|
if (is_dir($directory) && !is_writable($directory)) {
|
||||||
$io->error('Cannot write in directory: ' . $directory);
|
$io->error('Cannot write in directory: ' . $directory);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +142,7 @@ class CreateReleaseCommand extends Command
|
|||||||
passthru($command, $exitCode);
|
passthru($command, $exitCode);
|
||||||
if ($exitCode !== 0) {
|
if ($exitCode !== 0) {
|
||||||
$io->error('Failed with command: ' . $command);
|
$io->error('Failed with command: ' . $command);
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ EOT
|
|||||||
|
|
||||||
if (getenv('APP_ENV') === 'prod') {
|
if (getenv('APP_ENV') === 'prod') {
|
||||||
$io->error('kimai:reset-dev is not allowed in production');
|
$io->error('kimai:reset-dev is not allowed in production');
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/Configuration/ConfigLoaderInterface.php
Normal file
21
src/Configuration/ConfigLoaderInterface.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
|
||||||
|
interface ConfigLoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param null|string $prefix
|
||||||
|
* @return Configuration[]
|
||||||
|
*/
|
||||||
|
public function getConfiguration(?string $prefix = null): array;
|
||||||
|
}
|
||||||
35
src/Configuration/FormConfiguration.php
Normal file
35
src/Configuration/FormConfiguration.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
class FormConfiguration implements SystemBundleConfiguration
|
||||||
|
{
|
||||||
|
use StringAccessibleConfigTrait;
|
||||||
|
|
||||||
|
public function getPrefix(): string
|
||||||
|
{
|
||||||
|
return 'defaults';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomerDefaultTimezone(): string
|
||||||
|
{
|
||||||
|
return $this->find('customer.timezone');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomerDefaultCurrency(): string
|
||||||
|
{
|
||||||
|
return $this->find('customer.currency');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomerDefaultCountry(): string
|
||||||
|
{
|
||||||
|
return $this->find('customer.country');
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/Configuration/StringAccessibleConfigTrait.php
Normal file
123
src/Configuration/StringAccessibleConfigTrait.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
|
||||||
|
trait StringAccessibleConfigTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $settings;
|
||||||
|
/**
|
||||||
|
* @var ConfigLoaderInterface
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $settings
|
||||||
|
*/
|
||||||
|
public function __construct(ConfigLoaderInterface $repository, array $settings)
|
||||||
|
{
|
||||||
|
$this->repository = $repository;
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConfigLoaderInterface $repository
|
||||||
|
* @return Configuration[]
|
||||||
|
*/
|
||||||
|
protected function getConfigurations(ConfigLoaderInterface $repository): array
|
||||||
|
{
|
||||||
|
return $repository->getConfiguration($this->getPrefix() . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepare()
|
||||||
|
{
|
||||||
|
if ($this->initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this foreach should be replaced by a better piece of code,
|
||||||
|
// especially the pointers could be a problem in the future
|
||||||
|
foreach ($this->getConfigurations($this->repository) as $configuration) {
|
||||||
|
$temp = explode('.', $configuration->getName());
|
||||||
|
$array = &$this->settings;
|
||||||
|
if ($temp[0] === $this->getPrefix()) {
|
||||||
|
$temp = array_slice($temp, 1);
|
||||||
|
}
|
||||||
|
foreach ($temp as $key2) {
|
||||||
|
if (!isset($array[$key2])) {
|
||||||
|
// unknown values will silently be skipped
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
if (is_array($array[$key2])) {
|
||||||
|
$array = &$array[$key2];
|
||||||
|
} elseif (is_bool($array[$key2])) {
|
||||||
|
$array[$key2] = (bool) $configuration->getValue();
|
||||||
|
} elseif (is_int($array[$key2])) {
|
||||||
|
$array[$key2] = (int) $configuration->getValue();
|
||||||
|
} else {
|
||||||
|
$array[$key2] = $configuration->getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract protected function getPrefix(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function find(string $key)
|
||||||
|
{
|
||||||
|
$this->prepare();
|
||||||
|
$prefix = $this->getPrefix() . '.';
|
||||||
|
$length = strlen($prefix);
|
||||||
|
|
||||||
|
if (substr($key, 0, $length) === $prefix) {
|
||||||
|
$key = substr($key, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->get($key, $this->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @param array $config
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function get(string $key, array $config)
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
$search = array_shift($keys);
|
||||||
|
|
||||||
|
if (!isset($config[$search])) {
|
||||||
|
throw new \InvalidArgumentException('Unknown config: ' . $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($config[$search]) && !empty($keys)) {
|
||||||
|
return $this->get(implode('.', $keys), $config[$search]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config[$search];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Configuration/SystemBundleConfiguration.php
Normal file
24
src/Configuration/SystemBundleConfiguration.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
interface SystemBundleConfiguration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPrefix(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function find(string $key);
|
||||||
|
}
|
||||||
25
src/Configuration/SystemConfiguration.php
Normal file
25
src/Configuration/SystemConfiguration.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
class SystemConfiguration implements SystemBundleConfiguration
|
||||||
|
{
|
||||||
|
use StringAccessibleConfigTrait;
|
||||||
|
|
||||||
|
public function getPrefix(): string
|
||||||
|
{
|
||||||
|
return 'kimai';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getConfigurations(ConfigLoaderInterface $repository): array
|
||||||
|
{
|
||||||
|
return $repository->getConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Configuration/TimesheetConfiguration.php
Normal file
45
src/Configuration/TimesheetConfiguration.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
class TimesheetConfiguration implements SystemBundleConfiguration
|
||||||
|
{
|
||||||
|
use StringAccessibleConfigTrait;
|
||||||
|
|
||||||
|
public function getPrefix(): string
|
||||||
|
{
|
||||||
|
return 'timesheet';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowFutureTimes(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->find('rules.allow_future_times');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDurationOnly(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->find('duration_only');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMarkdownEnabled(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->find('markdown_content');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveEntriesHardLimit(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('active_entries.hard_limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getActiveEntriesSoftLimit(): int
|
||||||
|
{
|
||||||
|
return (int) $this->find('active_entries.soft_limit');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\FormConfiguration;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Form\CustomerEditForm;
|
use App\Form\CustomerEditForm;
|
||||||
use App\Form\Toolbar\CustomerToolbarForm;
|
use App\Form\Toolbar\CustomerToolbarForm;
|
||||||
@@ -30,16 +31,22 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
class CustomerController extends AbstractController
|
class CustomerController extends AbstractController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var CustomerRepository
|
||||||
*/
|
*/
|
||||||
private $defaults;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var FormConfiguration
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $defaults
|
* @param CustomerRepository $repository
|
||||||
|
* @param FormConfiguration $configuration
|
||||||
*/
|
*/
|
||||||
public function __construct(array $defaults)
|
public function __construct(CustomerRepository $repository, FormConfiguration $configuration)
|
||||||
{
|
{
|
||||||
$this->defaults = $defaults;
|
$this->repository = $repository;
|
||||||
|
$this->configuration = $configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +54,7 @@ class CustomerController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
protected function getRepository()
|
protected function getRepository()
|
||||||
{
|
{
|
||||||
return $this->getDoctrine()->getRepository(Customer::class);
|
return $this->repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,9 +102,9 @@ class CustomerController extends AbstractController
|
|||||||
public function createAction(Request $request)
|
public function createAction(Request $request)
|
||||||
{
|
{
|
||||||
$customer = new Customer();
|
$customer = new Customer();
|
||||||
$customer->setCountry($this->defaults['customer']['country']);
|
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||||
$customer->setCurrency($this->defaults['customer']['currency']);
|
$customer->setCurrency($this->configuration->getCustomerDefaultCurrency());
|
||||||
$customer->setTimezone($this->defaults['customer']['timezone']);
|
$customer->setTimezone($this->configuration->getCustomerDefaultTimezone());
|
||||||
|
|
||||||
return $this->renderCustomerForm($customer, $request);
|
return $this->renderCustomerForm($customer, $request);
|
||||||
}
|
}
|
||||||
|
|||||||
246
src/Controller/SystemConfigurationController.php
Normal file
246
src/Controller/SystemConfigurationController.php
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<?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\Configuration\SystemConfiguration;
|
||||||
|
use App\Event\SystemConfigurationEvent;
|
||||||
|
use App\Form\Model\Configuration;
|
||||||
|
use App\Form\Model\SystemConfiguration as SystemConfigurationModel;
|
||||||
|
use App\Form\SystemConfigurationForm;
|
||||||
|
use App\Repository\ConfigurationRepository;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CountryType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used for executing system relevant tasks.
|
||||||
|
*
|
||||||
|
* @Route(path="/admin/system-config")
|
||||||
|
* @Security("is_granted('system_configuration')")
|
||||||
|
*/
|
||||||
|
class SystemConfigurationController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EventDispatcherInterface
|
||||||
|
*/
|
||||||
|
protected $eventDispatcher;
|
||||||
|
/**
|
||||||
|
* @var SystemConfiguration
|
||||||
|
*/
|
||||||
|
protected $configurations;
|
||||||
|
/**
|
||||||
|
* @var ConfigurationRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param EventDispatcherInterface $dispatcher
|
||||||
|
* @param ConfigurationRepository $repository
|
||||||
|
* @param SystemConfiguration $config
|
||||||
|
*/
|
||||||
|
public function __construct(EventDispatcherInterface $dispatcher, ConfigurationRepository $repository, SystemConfiguration $config)
|
||||||
|
{
|
||||||
|
$this->eventDispatcher = $dispatcher;
|
||||||
|
$this->repository = $repository;
|
||||||
|
$this->configurations = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/", name="system_configuration", methods={"GET"})
|
||||||
|
*
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function indexAction()
|
||||||
|
{
|
||||||
|
$configSettings = $this->getInitializedConfigurations();
|
||||||
|
|
||||||
|
$configurations = [];
|
||||||
|
foreach ($configSettings as $configModel) {
|
||||||
|
$configurations[] = [
|
||||||
|
'model' => $configModel,
|
||||||
|
'form' => $this->createConfigurationsForm($configModel)->createView(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('system-configuration/index.html.twig', [
|
||||||
|
'sections' => $configurations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/timesheet", name="system_configuration_timesheet", methods={"POST"})
|
||||||
|
*
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function timesheet(Request $request)
|
||||||
|
{
|
||||||
|
return $this->handleConfigUpdate($request, SystemConfigurationModel::SECTION_TIMESHEET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/customer", name="system_configuration_form_customer", methods={"POST"})
|
||||||
|
*
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function formDefaults(Request $request)
|
||||||
|
{
|
||||||
|
return $this->handleConfigUpdate($request, SystemConfigurationModel::SECTION_FORM_CUSTOMER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $section
|
||||||
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
protected function handleConfigUpdate(Request $request, string $section)
|
||||||
|
{
|
||||||
|
$configSettings = $this->getInitializedConfigurations();
|
||||||
|
foreach ($configSettings as $configModel) {
|
||||||
|
if ($configModel->getSection() === $section) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $configModel) {
|
||||||
|
throw $this->createNotFoundException('Could not find config model: ' . $section);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createConfigurationsForm($configModel);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
try {
|
||||||
|
$this->repository->saveSystemConfiguration($form->getData());
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('system_configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
$configSettings = $this->getInitializedConfigurations();
|
||||||
|
|
||||||
|
$configurations = [];
|
||||||
|
foreach ($configSettings as $configModel) {
|
||||||
|
if ($section !== $configModel->getSection()) {
|
||||||
|
$form2 = $this->createConfigurationsForm($configModel);
|
||||||
|
} else {
|
||||||
|
$form2 = $form;
|
||||||
|
}
|
||||||
|
$configurations[] = [
|
||||||
|
'model' => $configModel,
|
||||||
|
'form' => $form2->createView(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('system-configuration/index.html.twig', [
|
||||||
|
'sections' => $configurations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SystemConfigurationModel $configuration
|
||||||
|
* @return \Symfony\Component\Form\FormInterface
|
||||||
|
*/
|
||||||
|
private function createConfigurationsForm(SystemConfigurationModel $configuration)
|
||||||
|
{
|
||||||
|
return $this->createForm(
|
||||||
|
SystemConfigurationForm::class,
|
||||||
|
$configuration,
|
||||||
|
[
|
||||||
|
'attr' => ['id' => 'system_configuration_form_' . $configuration->getSection()],
|
||||||
|
'action' => $this->generateUrl('system_configuration_' . $configuration->getSection()),
|
||||||
|
'method' => 'POST'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SystemConfigurationModel[]
|
||||||
|
*/
|
||||||
|
protected function getInitializedConfigurations()
|
||||||
|
{
|
||||||
|
$types = $this->getConfigurationTypes();
|
||||||
|
|
||||||
|
$event = new SystemConfigurationEvent($types);
|
||||||
|
$this->eventDispatcher->dispatch(SystemConfigurationEvent::CONFIGURE, $event);
|
||||||
|
|
||||||
|
foreach ($event->getConfigurations() as $configs) {
|
||||||
|
foreach ($configs->getConfiguration() as $config) {
|
||||||
|
$config->setValue($this->configurations->find($config->getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event->getConfigurations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SystemConfigurationModel[]
|
||||||
|
*/
|
||||||
|
protected function getConfigurationTypes()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new SystemConfigurationModel())
|
||||||
|
->setSection(SystemConfigurationModel::SECTION_TIMESHEET)
|
||||||
|
->setConfiguration([
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.markdown_content')
|
||||||
|
->setType(CheckboxType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.duration_only')
|
||||||
|
->setType(CheckboxType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.rules.allow_future_times')
|
||||||
|
->setType(CheckboxType::class)
|
||||||
|
->setTranslationDomain('system-configuration'),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.active_entries.hard_limit')
|
||||||
|
->setType(IntegerType::class)
|
||||||
|
->setTranslationDomain('system-configuration')
|
||||||
|
->setConstraints([
|
||||||
|
new GreaterThanOrEqual(['value' => 1])
|
||||||
|
]),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('timesheet.active_entries.soft_limit')
|
||||||
|
->setType(IntegerType::class)
|
||||||
|
->setTranslationDomain('system-configuration')
|
||||||
|
->setConstraints([
|
||||||
|
new GreaterThanOrEqual(['value' => 1])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
(new SystemConfigurationModel())
|
||||||
|
->setSection(SystemConfigurationModel::SECTION_FORM_CUSTOMER)
|
||||||
|
->setConfiguration([
|
||||||
|
(new Configuration())
|
||||||
|
->setName('defaults.customer.timezone')
|
||||||
|
->setLabel('timezone')
|
||||||
|
->setType(TimezoneType::class),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('defaults.customer.country')
|
||||||
|
->setLabel('country')
|
||||||
|
->setType(CountryType::class),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('defaults.customer.currency')
|
||||||
|
->setLabel('currency')
|
||||||
|
->setType(CurrencyType::class),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ class TimesheetController extends AbstractController
|
|||||||
'showFilter' => $form->isSubmitted(),
|
'showFilter' => $form->isSubmitted(),
|
||||||
'toolbarForm' => $form->createView(),
|
'toolbarForm' => $form->createView(),
|
||||||
'showSummary' => $this->getUser()->getPreferenceValue('timesheet.daily_stats', false),
|
'showSummary' => $this->getUser()->getPreferenceValue('timesheet.daily_stats', false),
|
||||||
|
'duration_only' => $this->configuration->isDurationOnly(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +125,10 @@ class TimesheetController extends AbstractController
|
|||||||
|
|
||||||
return $this->render(
|
return $this->render(
|
||||||
'navbar/active-entries.html.twig',
|
'navbar/active-entries.html.twig',
|
||||||
['entries' => $activeEntries]
|
[
|
||||||
|
'entries' => $activeEntries,
|
||||||
|
'soft_limit' => $this->getSoftLimit(),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
@@ -23,32 +24,23 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
*/
|
*/
|
||||||
trait TimesheetControllerTrait
|
trait TimesheetControllerTrait
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
private $hardLimit = 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
*/
|
*/
|
||||||
protected $dateTime;
|
protected $dateTime;
|
||||||
|
/**
|
||||||
|
* @var TimesheetConfiguration
|
||||||
|
*/
|
||||||
|
protected $configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param UserDateTimeFactory $dateTime
|
* @param UserDateTimeFactory $dateTime
|
||||||
* @param int $hardLimit
|
* @param TimesheetConfiguration $configuration
|
||||||
*/
|
*/
|
||||||
public function __construct(UserDateTimeFactory $dateTime, int $hardLimit)
|
public function __construct(UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||||
{
|
{
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
$this->setHardLimit($hardLimit);
|
$this->configuration = $configuration;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $hardLimit
|
|
||||||
*/
|
|
||||||
protected function setHardLimit(int $hardLimit)
|
|
||||||
{
|
|
||||||
$this->hardLimit = $hardLimit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +48,15 @@ trait TimesheetControllerTrait
|
|||||||
*/
|
*/
|
||||||
protected function getHardLimit()
|
protected function getHardLimit()
|
||||||
{
|
{
|
||||||
return $this->hardLimit;
|
return $this->configuration->getActiveEntriesHardLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getSoftLimit()
|
||||||
|
{
|
||||||
|
return $this->configuration->getActiveEntriesSoftLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class TimesheetTeamController extends AbstractController
|
|||||||
'query' => $query,
|
'query' => $query,
|
||||||
'showFilter' => $form->isSubmitted(),
|
'showFilter' => $form->isSubmitted(),
|
||||||
'toolbarForm' => $form->createView(),
|
'toolbarForm' => $form->createView(),
|
||||||
|
'duration_only' => $this->configuration->isDurationOnly(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ namespace App\DependencyInjection;
|
|||||||
|
|
||||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class that loads and manages the Kimai configuration and container parameter.
|
* This class that loads and manages the Kimai configuration and container parameter.
|
||||||
*/
|
*/
|
||||||
class AppExtension extends Extension implements PrependExtensionInterface
|
class AppExtension extends Extension
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array $configs
|
* @param array $configs
|
||||||
@@ -34,8 +33,8 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// safe alternatives to %kernel.project_dir%
|
// safe alternatives to %kernel.project_dir%
|
||||||
$container->setParameter('kimai.data_dir', $container->getParameter('kernel.project_dir') . '/var/data');
|
$container->setParameter('kimai.data_dir', $config['data_dir']);
|
||||||
$container->setParameter('kimai.plugin_dir', $container->getParameter('kernel.project_dir') . '/var/plugins');
|
$container->setParameter('kimai.plugin_dir', $config['plugin_dir']);
|
||||||
|
|
||||||
$container->setParameter('kimai.languages', $config['languages']);
|
$container->setParameter('kimai.languages', $config['languages']);
|
||||||
$container->setParameter('kimai.calendar', $config['calendar']);
|
$container->setParameter('kimai.calendar', $config['calendar']);
|
||||||
@@ -47,7 +46,12 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
$this->createPermissionParameter($config['permissions'], $container);
|
$this->createPermissionParameter($config['permissions'], $container);
|
||||||
$this->createThemeParameter($config['theme'], $container);
|
$this->createThemeParameter($config['theme'], $container);
|
||||||
$this->createUserParameter($config['user'], $container);
|
$this->createUserParameter($config['user'], $container);
|
||||||
$this->createTimesheetParameter($config['timesheet'], $container);
|
|
||||||
|
$container->setParameter('kimai.config', $config);
|
||||||
|
|
||||||
|
$container->setParameter('kimai.timesheet', $config['timesheet']);
|
||||||
|
$container->setParameter('kimai.timesheet.rates', $config['timesheet']['rates']);
|
||||||
|
$container->setParameter('kimai.timesheet.rounding', $config['timesheet']['rounding']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,47 +116,6 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
$container->setParameter('kimai.fosuser', $config);
|
$container->setParameter('kimai.fosuser', $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $config
|
|
||||||
* @param ContainerBuilder $container
|
|
||||||
*/
|
|
||||||
protected function createTimesheetParameter(array $config, ContainerBuilder $container)
|
|
||||||
{
|
|
||||||
$container->setParameter('kimai.timesheet.rules', $config['rules']);
|
|
||||||
$container->setParameter('kimai.timesheet.rates', $config['rates']);
|
|
||||||
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
|
|
||||||
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
|
|
||||||
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
|
|
||||||
$container->setParameter('kimai.timesheet.active_entries.soft_limit', $config['active_entries']['soft_limit']);
|
|
||||||
$container->setParameter('kimai.timesheet.active_entries.hard_limit', $config['active_entries']['hard_limit']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ContainerBuilder $container
|
|
||||||
*/
|
|
||||||
public function prepend(ContainerBuilder $container)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
$configuration = new Configuration();
|
|
||||||
$configs = $container->getExtensionConfig($this->getAlias());
|
|
||||||
try {
|
|
||||||
$config = $this->processConfiguration($configuration, $configs);
|
|
||||||
} catch (InvalidConfigurationException $e) {
|
|
||||||
trigger_error('Found invalid "kimai" configuration: ' . $e->getMessage());
|
|
||||||
$config = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$container->prependExtensionConfig(
|
|
||||||
'twig',
|
|
||||||
[
|
|
||||||
'globals' => [
|
|
||||||
'duration_only' => $config['timesheet']['duration_only'],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,12 +25,8 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
|||||||
{
|
{
|
||||||
$twig = $container->getDefinition('twig');
|
$twig = $container->getDefinition('twig');
|
||||||
$theme = $container->getParameter('kimai.theme');
|
$theme = $container->getParameter('kimai.theme');
|
||||||
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
|
|
||||||
|
|
||||||
$twig->addMethodCall('addGlobal', ['kimai_context', array_merge($theme, [
|
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
||||||
'active_warning' => $container->getParameter('kimai.timesheet.active_entries.soft_limit')
|
|
||||||
])]);
|
|
||||||
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
|
|
||||||
|
|
||||||
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
||||||
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
$definition = $container->getDefinition('twig.loader.native_filesystem');
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ class Configuration implements ConfigurationInterface
|
|||||||
|
|
||||||
$rootNode
|
$rootNode
|
||||||
->children()
|
->children()
|
||||||
|
->scalarNode('data_dir')
|
||||||
|
->isRequired()
|
||||||
|
->validate()
|
||||||
|
->ifTrue(function ($value) {
|
||||||
|
return !file_exists($value);
|
||||||
|
})
|
||||||
|
->thenInvalid('Data directory does not exist')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->scalarNode('plugin_dir')
|
||||||
|
->isRequired()
|
||||||
|
->validate()
|
||||||
|
->ifTrue(function ($value) {
|
||||||
|
return !file_exists($value);
|
||||||
|
})
|
||||||
|
->thenInvalid('Plugin directory does not exist')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
->append($this->getUserNode())
|
->append($this->getUserNode())
|
||||||
->append($this->getTimesheetNode())
|
->append($this->getTimesheetNode())
|
||||||
->append($this->getInvoiceNode())
|
->append($this->getInvoiceNode())
|
||||||
@@ -85,7 +103,6 @@ class Configuration implements ConfigurationInterface
|
|||||||
->scalarNode('mode')
|
->scalarNode('mode')
|
||||||
->defaultValue('default')
|
->defaultValue('default')
|
||||||
->validate()
|
->validate()
|
||||||
->thenInvalid('Chosen rounding mode is invalid')
|
|
||||||
->ifTrue(function ($value) {
|
->ifTrue(function ($value) {
|
||||||
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
|
$class = 'App\\Timesheet\\Rounding\\' . ucfirst($value) . 'Rounding';
|
||||||
if (class_exists($class)) {
|
if (class_exists($class)) {
|
||||||
|
|||||||
99
src/Entity/Configuration.php
Normal file
99
src/Entity/Configuration.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")
|
||||||
|
* @ORM\Table(
|
||||||
|
* name="configuration",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* @UniqueEntity("name")
|
||||||
|
*/
|
||||||
|
class Configuration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue
|
||||||
|
* @ORM\Column(name="id", type="integer")
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||||
|
* @Assert\Length(min=2, max=100)
|
||||||
|
*/
|
||||||
|
private $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||||
|
*/
|
||||||
|
private $value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setName(string $name): Configuration
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given $value will not be serialized before its stored, so it should be a scalar type.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setValue($value): Configuration
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getValue(): ?string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Event/SystemConfigurationEvent.php
Normal file
53
src/Event/SystemConfigurationEvent.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?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\Form\Model\SystemConfiguration;
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event should be used, if system configurations should be changed/added dynamically.
|
||||||
|
*/
|
||||||
|
class SystemConfigurationEvent extends Event
|
||||||
|
{
|
||||||
|
public const CONFIGURE = 'app.system_configuration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SystemConfiguration[]
|
||||||
|
*/
|
||||||
|
protected $preferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SystemConfiguration[] $configurations
|
||||||
|
*/
|
||||||
|
public function __construct(array $configurations)
|
||||||
|
{
|
||||||
|
$this->preferences = $configurations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return SystemConfiguration[]
|
||||||
|
*/
|
||||||
|
public function getConfigurations()
|
||||||
|
{
|
||||||
|
return $this->preferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param SystemConfiguration $configuration
|
||||||
|
* @return SystemConfigurationEvent
|
||||||
|
*/
|
||||||
|
public function addConfiguration(SystemConfiguration $configuration): SystemConfigurationEvent
|
||||||
|
{
|
||||||
|
$this->preferences[] = $configuration;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,5 +120,11 @@ class MenuSubscriber implements EventSubscriberInterface
|
|||||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($auth->isGranted('system_configuration')) {
|
||||||
|
$menu->addChild(
|
||||||
|
new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], 'fas fa-cogs')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
177
src/Form/Model/Configuration.php
Normal file
177
src/Form/Model/Configuration.php
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<?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\Form\Model;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
class Configuration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $name;
|
||||||
|
/**
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private $label;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $translationDomain = 'messages';
|
||||||
|
/**
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
private $value;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $type;
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $enabled = true;
|
||||||
|
/**
|
||||||
|
* @var Constraint[]
|
||||||
|
*/
|
||||||
|
protected $constraints = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setName(string $name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getValue()
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setValue($value)
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $type
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setType(string $type)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $enabled
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setEnabled(bool $enabled)
|
||||||
|
{
|
||||||
|
$this->enabled = $enabled;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getLabel()
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $label
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setLabel(string $label)
|
||||||
|
{
|
||||||
|
$this->label = $label;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Constraint[]
|
||||||
|
*/
|
||||||
|
public function getConstraints(): array
|
||||||
|
{
|
||||||
|
return $this->constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Constraint[] $constraints
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setConstraints(array $constraints)
|
||||||
|
{
|
||||||
|
$this->constraints = $constraints;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTranslationDomain()
|
||||||
|
{
|
||||||
|
return $this->translationDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $translationDomain
|
||||||
|
* @return Configuration
|
||||||
|
*/
|
||||||
|
public function setTranslationDomain(string $translationDomain)
|
||||||
|
{
|
||||||
|
$this->translationDomain = $translationDomain;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Form/Model/SystemConfiguration.php
Normal file
63
src/Form/Model/SystemConfiguration.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?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\Form\Model;
|
||||||
|
|
||||||
|
class SystemConfiguration
|
||||||
|
{
|
||||||
|
public const SECTION_TIMESHEET = 'timesheet';
|
||||||
|
public const SECTION_FORM_CUSTOMER = 'form_customer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $section;
|
||||||
|
/**
|
||||||
|
* @var Configuration[]
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSection(): string
|
||||||
|
{
|
||||||
|
return $this->section;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $section
|
||||||
|
* @return SystemConfiguration
|
||||||
|
*/
|
||||||
|
public function setSection(string $section)
|
||||||
|
{
|
||||||
|
$this->section = $section;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Configuration[]
|
||||||
|
*/
|
||||||
|
public function getConfiguration(): array
|
||||||
|
{
|
||||||
|
return $this->configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Configuration[] $configuration
|
||||||
|
* @return SystemConfiguration
|
||||||
|
*/
|
||||||
|
public function setConfiguration(array $configuration)
|
||||||
|
{
|
||||||
|
$this->configuration = $configuration;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Form/SystemConfigurationForm.php
Normal file
50
src/Form/SystemConfigurationForm.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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\Form;
|
||||||
|
|
||||||
|
use App\Form\Model\SystemConfiguration;
|
||||||
|
use App\Form\Type\SystemConfigurationType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used to edit one section within the system configuration.
|
||||||
|
*/
|
||||||
|
class SystemConfigurationForm extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->add('configuration', CollectionType::class, [
|
||||||
|
'entry_type' => SystemConfigurationType::class,
|
||||||
|
'entry_options' => ['label' => false],
|
||||||
|
'allow_add' => false,
|
||||||
|
'allow_delete' => false,
|
||||||
|
'label' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => SystemConfiguration::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'edit_user_preferences',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Form;
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Form\Type\ActivityType;
|
use App\Form\Type\ActivityType;
|
||||||
use App\Form\Type\CustomerType;
|
use App\Form\Type\CustomerType;
|
||||||
@@ -38,34 +39,31 @@ class TimesheetEditForm extends AbstractType
|
|||||||
* @var CustomerRepository
|
* @var CustomerRepository
|
||||||
*/
|
*/
|
||||||
private $customers;
|
private $customers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ProjectRepository
|
* @var ProjectRepository
|
||||||
*/
|
*/
|
||||||
private $projects;
|
private $projects;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $durationOnly = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UserDateTimeFactory
|
* @var UserDateTimeFactory
|
||||||
*/
|
*/
|
||||||
protected $dateTime;
|
protected $dateTime;
|
||||||
|
/**
|
||||||
|
* @var TimesheetConfiguration
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CustomerRepository $customer
|
* @param CustomerRepository $customer
|
||||||
* @param ProjectRepository $project
|
* @param ProjectRepository $project
|
||||||
* @param UserDateTimeFactory $dateTime
|
* @param UserDateTimeFactory $dateTime
|
||||||
* @param bool $durationOnly
|
* @param TimesheetConfiguration $config
|
||||||
*/
|
*/
|
||||||
public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime, bool $durationOnly)
|
public function __construct(CustomerRepository $customer, ProjectRepository $project, UserDateTimeFactory $dateTime, TimesheetConfiguration $config)
|
||||||
{
|
{
|
||||||
$this->customers = $customer;
|
$this->customers = $customer;
|
||||||
$this->projects = $project;
|
$this->projects = $project;
|
||||||
$this->dateTime = $dateTime;
|
$this->dateTime = $dateTime;
|
||||||
$this->durationOnly = $durationOnly;
|
$this->configuration = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +104,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
$timezone = $begin->getTimezone()->getName();
|
$timezone = $begin->getTimezone()->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $end || !$options['duration_only']) {
|
if (null === $end || !$this->configuration->isDurationOnly()) {
|
||||||
$builder->add('begin', DateTimePickerType::class, [
|
$builder->add('begin', DateTimePickerType::class, [
|
||||||
'label' => 'label.begin',
|
'label' => 'label.begin',
|
||||||
'model_timezone' => $timezone,
|
'model_timezone' => $timezone,
|
||||||
@@ -114,7 +112,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($options['duration_only']) {
|
if ($this->configuration->isDurationOnly()) {
|
||||||
$builder->add('duration', DurationType::class, [
|
$builder->add('duration', DurationType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'docu_chapter' => 'timesheet.html#duration-format',
|
'docu_chapter' => 'timesheet.html#duration-format',
|
||||||
@@ -303,7 +301,6 @@ class TimesheetEditForm extends AbstractType
|
|||||||
'csrf_protection' => true,
|
'csrf_protection' => true,
|
||||||
'csrf_field_name' => '_token',
|
'csrf_field_name' => '_token',
|
||||||
'csrf_token_id' => 'timesheet_edit',
|
'csrf_token_id' => 'timesheet_edit',
|
||||||
'duration_only' => $this->durationOnly,
|
|
||||||
'include_user' => false,
|
'include_user' => false,
|
||||||
'include_exported' => false,
|
'include_exported' => false,
|
||||||
'include_rate' => true,
|
'include_rate' => true,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class DateTimePickerType extends AbstractType
|
|||||||
if ($options['autofocus']) {
|
if ($options['autofocus']) {
|
||||||
$values['autofocus'] = 'autofocus';
|
$values['autofocus'] = 'autofocus';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $values;
|
return $values;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/Form/Type/SystemConfigurationType.php
Normal file
75
src/Form/Type/SystemConfigurationType.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use App\Form\Model\Configuration;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormEvent;
|
||||||
|
use Symfony\Component\Form\FormEvents;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to edit a system configuration.
|
||||||
|
*/
|
||||||
|
class SystemConfigurationType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param FormBuilderInterface $builder
|
||||||
|
* @param array $options
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder->addEventListener(
|
||||||
|
FormEvents::PRE_SET_DATA,
|
||||||
|
function (FormEvent $event) {
|
||||||
|
/** @var Configuration $preference */
|
||||||
|
$preference = $event->getData();
|
||||||
|
|
||||||
|
if (!($preference instanceof Configuration)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevents unconfigured values from showing up in the form
|
||||||
|
if ($preference->getType() === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$required = true;
|
||||||
|
if (CheckboxType::class == $preference->getType()) {
|
||||||
|
$required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $preference->getType();
|
||||||
|
if (!$preference->isEnabled()) {
|
||||||
|
$type = HiddenType::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->getForm()->add('value', $type, [
|
||||||
|
'label' => 'label.' . ($preference->getLabel() ?? $preference->getName()),
|
||||||
|
'constraints' => $preference->getConstraints(),
|
||||||
|
'required' => $required,
|
||||||
|
'disabled' => !$preference->isEnabled(),
|
||||||
|
'translation_domain' => $preference->getTranslationDomain(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$builder->add('name', HiddenType::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Configuration::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,10 @@ class Kernel extends BaseKernel
|
|||||||
|
|
||||||
// load plugin routes
|
// load plugin routes
|
||||||
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
$pluginsDir = $this->getProjectDir() . '/var/plugins';
|
||||||
|
if (!file_exists($pluginsDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$routes->import($pluginsDir . '/*Bundle/Resources/config/routes' . self::CONFIG_EXTS, '/', 'glob');
|
$routes->import($pluginsDir . '/*Bundle/Resources/config/routes' . self::CONFIG_EXTS, '/', 'glob');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
src/Migrations/Version20190321181243.php
Normal file
43
src/Migrations/Version20190321181243.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the system configuration table.
|
||||||
|
*
|
||||||
|
* @version 0.9
|
||||||
|
*/
|
||||||
|
final class Version20190321181243 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
if ($this->isPlatformSqlite()) {
|
||||||
|
$this->addSql('CREATE TABLE kimai2_configuration (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(100) NOT NULL, value VARCHAR(255) DEFAULT NULL)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_1C5D63D85E237E06 ON kimai2_configuration (name)');
|
||||||
|
} else {
|
||||||
|
$this->addSql('CREATE TABLE kimai2_configuration (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, value VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_1C5D63D85E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE kimai2_configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Repository/ConfigurationRepository.php
Normal file
66
src/Repository/ConfigurationRepository.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?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\Repository;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
use App\Form\Model\SystemConfiguration;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
|
use Doctrine\ORM\Query;
|
||||||
|
|
||||||
|
class ConfigurationRepository extends AbstractRepository implements ConfigLoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $prefix
|
||||||
|
* @return Configuration[]
|
||||||
|
*/
|
||||||
|
public function getConfiguration(?string $prefix = null): array
|
||||||
|
{
|
||||||
|
if (null === $prefix) {
|
||||||
|
return $this->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->createQueryBuilder('c');
|
||||||
|
$qb
|
||||||
|
->select('c')
|
||||||
|
->where($qb->expr()->like('c.name', ':prefix'))
|
||||||
|
->setParameter(':prefix', $prefix . '%');
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult(Query::HYDRATE_OBJECT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveSystemConfiguration(SystemConfiguration $model)
|
||||||
|
{
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($model->getConfiguration() as $configuration) {
|
||||||
|
$entity = $this->findOneBy(['name' => $configuration->getName()]);
|
||||||
|
$value = $configuration->getValue();
|
||||||
|
|
||||||
|
if (null === $entity) {
|
||||||
|
$entity = new Configuration();
|
||||||
|
$entity->setName($configuration->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity->setValue($value);
|
||||||
|
|
||||||
|
$em->persist($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->flush();
|
||||||
|
$em->commit();
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$em->rollback();
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Twig;
|
namespace App\Twig;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Utils\Markdown;
|
use App\Utils\Markdown;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
@@ -23,18 +24,18 @@ class MarkdownExtension extends AbstractExtension
|
|||||||
*/
|
*/
|
||||||
private $markdown;
|
private $markdown;
|
||||||
/**
|
/**
|
||||||
* @var bool
|
* @var TimesheetConfiguration
|
||||||
*/
|
*/
|
||||||
private $timesheetIsMarkdown = false;
|
protected $configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MarkdownExtension constructor.
|
* MarkdownExtension constructor.
|
||||||
* @param Markdown $parser
|
* @param Markdown $parser
|
||||||
*/
|
*/
|
||||||
public function __construct(Markdown $parser, bool $timesheetAsMarkdown = false)
|
public function __construct(Markdown $parser, TimesheetConfiguration $configuration)
|
||||||
{
|
{
|
||||||
$this->markdown = $parser;
|
$this->markdown = $parser;
|
||||||
$this->timesheetIsMarkdown = $timesheetAsMarkdown;
|
$this->configuration = $configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +61,7 @@ class MarkdownExtension extends AbstractExtension
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->timesheetIsMarkdown) {
|
if ($this->configuration->isMarkdownEnabled()) {
|
||||||
return $this->markdown->toHtml($content, false);
|
return $this->markdown->toHtml($content, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Validator\Constraints;
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Timesheet as TimesheetEntity;
|
use App\Entity\Timesheet as TimesheetEntity;
|
||||||
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
use App\Validator\Constraints\Timesheet as TimesheetConstraint;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
@@ -24,38 +25,18 @@ class TimesheetValidator extends ConstraintValidator
|
|||||||
*/
|
*/
|
||||||
protected $auth;
|
protected $auth;
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var TimesheetConfiguration
|
||||||
*/
|
*/
|
||||||
protected $rules = [];
|
protected $configuration;
|
||||||
/**
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $durationOnly = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param AuthorizationCheckerInterface $auth
|
* @param AuthorizationCheckerInterface $auth
|
||||||
* @param array $ruleset
|
* @param TimesheetConfiguration $configuration
|
||||||
* @param bool $durationOnly
|
|
||||||
*/
|
*/
|
||||||
public function __construct(AuthorizationCheckerInterface $auth, array $ruleset, bool $durationOnly)
|
public function __construct(AuthorizationCheckerInterface $auth, TimesheetConfiguration $configuration)
|
||||||
{
|
{
|
||||||
$this->auth = $auth;
|
$this->auth = $auth;
|
||||||
$this->rules = $ruleset;
|
$this->configuration = $configuration;
|
||||||
$this->durationOnly = $durationOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $key
|
|
||||||
* @param null $default
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
|
||||||
protected function getRule(string $key, $default = null)
|
|
||||||
{
|
|
||||||
if (!isset($this->rules[$key])) {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->rules[$key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +70,7 @@ class TimesheetValidator extends ConstraintValidator
|
|||||||
if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) {
|
if ($context->getViolations()->count() == 0 && null === $timesheet->getEnd()) {
|
||||||
if (!$this->auth->isGranted('start', $timesheet)) {
|
if (!$this->auth->isGranted('start', $timesheet)) {
|
||||||
$context->buildViolation('You are not allowed to start this timesheet record.')
|
$context->buildViolation('You are not allowed to start this timesheet record.')
|
||||||
->atPath($this->durationOnly ? 'duration' : 'end')
|
->atPath($this->configuration->isDurationOnly() ? 'duration' : 'end')
|
||||||
->setTranslationDomain('validators')
|
->setTranslationDomain('validators')
|
||||||
->setCode(TimesheetConstraint::START_DISALLOWED)
|
->setCode(TimesheetConstraint::START_DISALLOWED)
|
||||||
->addViolation();
|
->addViolation();
|
||||||
@@ -125,7 +106,7 @@ class TimesheetValidator extends ConstraintValidator
|
|||||||
->addViolation();
|
->addViolation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->getRule('allow_future_times', true) && time() < $timesheet->getBegin()->getTimestamp()) {
|
if (false === $this->configuration->isAllowFutureTimes() && time() < $timesheet->getBegin()->getTimestamp()) {
|
||||||
$context->buildViolation('The begin date cannot be in the future.')
|
$context->buildViolation('The begin date cannot be in the future.')
|
||||||
->atPath('begin')
|
->atPath('begin')
|
||||||
->setTranslationDomain('validators')
|
->setTranslationDomain('validators')
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{% extends '@AdminLTE/layout/default-layout-avanzu.html.twig' %}
|
{% extends '@AdminLTE/layout/default-layout-avanzu.html.twig' %}
|
||||||
|
|
||||||
{% block avanzu_page_content_before %}
|
{% block avanzu_after_body_start %}
|
||||||
<div class="toolbar-pad no-print">
|
|
||||||
{% block main_before %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% embed 'embeds/modal.html.twig' %}
|
{% embed 'embeds/modal.html.twig' %}
|
||||||
{% block modal_id %}remote_form_modal{% endblock %}
|
{% block modal_id %}remote_form_modal{% endblock %}
|
||||||
{% block modal_title %}{% endblock %}
|
{% block modal_title %}{% endblock %}
|
||||||
@@ -12,6 +9,12 @@
|
|||||||
{% endembed %}
|
{% endembed %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block avanzu_page_content_before %}
|
||||||
|
<div class="toolbar-pad no-print">
|
||||||
|
{% block main_before %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block avanzu_page_content_after %}
|
{% block avanzu_page_content_after %}
|
||||||
{% block main_after %}{% endblock %}
|
{% block main_after %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<li class="dropdown messages-menu">
|
<li class="dropdown messages-menu">
|
||||||
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
|
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
|
||||||
<i class="{{ 'start'|icon }} fa-2x"></i>
|
<i class="{{ 'start'|icon }} fa-2x"></i>
|
||||||
<span class="label label-{% if entries|length > kimai_context.active_warning %}danger{% else %}warning{% endif %}">{{ entries|length }}</span>
|
<span class="label label-{% if entries|length > soft_limit %}danger{% elseif entries|length == soft_limit %}warning{% else %}primary{% endif %}">{{ entries|length }}</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="header">
|
<li class="header">
|
||||||
|
|||||||
15
templates/system-configuration/index.html.twig
Normal file
15
templates/system-configuration/index.html.twig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'title'|trans({}, 'system-configuration') }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'subtitle'|trans({}, 'system-configuration') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% for section in sections %}
|
||||||
|
{{ include('default/_form.html.twig', {
|
||||||
|
'title': section.model.section|trans({}, 'system-configuration'),
|
||||||
|
'form': section.form,
|
||||||
|
}) }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
100
tests/Configuration/FormConfigurationTest.php
Normal file
100
tests/Configuration/FormConfigurationTest.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Configuration\FormConfiguration;
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Configuration\FormConfiguration
|
||||||
|
* @covers \App\Configuration\StringAccessibleConfigTrait
|
||||||
|
*/
|
||||||
|
class FormConfigurationTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function getSut(array $settings, array $loaderSettings = [])
|
||||||
|
{
|
||||||
|
$loader = new TestConfigLoader($loaderSettings);
|
||||||
|
|
||||||
|
return new FormConfiguration($loader, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'customer' => [
|
||||||
|
'timezone' => 'Europe/London',
|
||||||
|
'currency' => 'GBP',
|
||||||
|
'country' => 'FR',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultLoaderSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
|
||||||
|
(new Configuration())->setName('defaults.customer.currency')->setValue('USD'),
|
||||||
|
(new Configuration())->setName('defaults.customer.country')->setValue('RU'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrefix()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('defaults', $sut->getPrefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithoutLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('Europe/London', $sut->getCustomerDefaultTimezone());
|
||||||
|
$this->assertEquals('GBP', $sut->getCustomerDefaultCurrency());
|
||||||
|
$this->assertEquals('FR', $sut->getCustomerDefaultCountry());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||||
|
$this->assertEquals('Russia/Moscov', $sut->getCustomerDefaultTimezone());
|
||||||
|
$this->assertEquals('USD', $sut->getCustomerDefaultCurrency());
|
||||||
|
$this->assertEquals('RU', $sut->getCustomerDefaultCountry());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithMixedConfigs()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('defaults.customer.country')->setValue('RU'),
|
||||||
|
(new Configuration())->setName('defaults.customer.foobar')->setValue('hello'),
|
||||||
|
]);
|
||||||
|
$this->assertEquals('Europe/London', $sut->getCustomerDefaultTimezone());
|
||||||
|
$this->assertEquals('GBP', $sut->getCustomerDefaultCurrency());
|
||||||
|
$this->assertEquals('RU', $sut->getCustomerDefaultCountry());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindByKey()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('FR', $sut->find('customer.country'));
|
||||||
|
$this->assertEquals('FR', $sut->find('defaults.customer.country'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Unknown config: foobar
|
||||||
|
*/
|
||||||
|
public function testUnknownConfigAreNotImportedAndFindingThemThrowsException()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('defaults.customer.foobar')->setValue('hello'),
|
||||||
|
]);
|
||||||
|
$this->assertEquals('hello', $sut->find('customer.foobar'));
|
||||||
|
}
|
||||||
|
}
|
||||||
114
tests/Configuration/SystemConfigurationTest.php
Normal file
114
tests/Configuration/SystemConfigurationTest.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Configuration\SystemConfiguration
|
||||||
|
* @covers \App\Configuration\StringAccessibleConfigTrait
|
||||||
|
*/
|
||||||
|
class SystemConfigurationTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array $settings
|
||||||
|
* @param array $loaderSettings
|
||||||
|
* @return SystemConfiguration
|
||||||
|
*/
|
||||||
|
protected function getSut(array $settings, array $loaderSettings = [])
|
||||||
|
{
|
||||||
|
$loader = new TestConfigLoader($loaderSettings);
|
||||||
|
|
||||||
|
return new SystemConfiguration($loader, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'timesheet' => [
|
||||||
|
'rules' => [
|
||||||
|
'allow_future_times' => false,
|
||||||
|
],
|
||||||
|
'duration_only' => true,
|
||||||
|
'markdown_content' => false,
|
||||||
|
'active_entries' => [
|
||||||
|
'hard_limit' => 99,
|
||||||
|
'soft_limit' => 15,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'defaults' => [
|
||||||
|
'customer' => [
|
||||||
|
'timezone' => 'Europe/London',
|
||||||
|
'currency' => 'GBP',
|
||||||
|
'country' => 'FR',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultLoaderSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
|
||||||
|
(new Configuration())->setName('defaults.customer.currency')->setValue('RUB'),
|
||||||
|
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue('1'),
|
||||||
|
(new Configuration())->setName('timesheet.duration_only')->setValue('0'),
|
||||||
|
(new Configuration())->setName('timesheet.markdown_content')->setValue('1'),
|
||||||
|
(new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'),
|
||||||
|
(new Configuration())->setName('timesheet.active_entries.soft_limit')->setValue('3'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrefix()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('kimai', $sut->getPrefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithoutLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('Europe/London', $sut->find('defaults.customer.timezone'));
|
||||||
|
$this->assertEquals('GBP', $sut->find('defaults.customer.currency'));
|
||||||
|
$this->assertEquals(false, $sut->find('timesheet.rules.allow_future_times'));
|
||||||
|
$this->assertEquals(99, $sut->find('timesheet.active_entries.hard_limit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||||
|
$this->assertEquals('Russia/Moscov', $sut->find('defaults.customer.timezone'));
|
||||||
|
$this->assertEquals('RUB', $sut->find('defaults.customer.currency'));
|
||||||
|
$this->assertEquals(true, $sut->find('timesheet.rules.allow_future_times'));
|
||||||
|
$this->assertEquals(7, $sut->find('timesheet.active_entries.hard_limit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithMixedConfigs()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('timesheet.duration_only')->setValue(''),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(false, $sut->find('timesheet.duration_only'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Unknown config: foo
|
||||||
|
*/
|
||||||
|
public function testUnknownConfigAreNotImportedAndFindingThemThrowsException()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('timesheet.foo')->setValue('hello'),
|
||||||
|
]);
|
||||||
|
$this->assertEquals('hello', $sut->find('foo'));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/Configuration/TestConfigLoader.php
Normal file
35
tests/Configuration/TestConfigLoader.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Configuration\FormConfiguration
|
||||||
|
*/
|
||||||
|
class TestConfigLoader implements ConfigLoaderInterface
|
||||||
|
{
|
||||||
|
private $configs = [];
|
||||||
|
|
||||||
|
public function __construct(array $configs)
|
||||||
|
{
|
||||||
|
$this->configs = $configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null|string $prefix
|
||||||
|
* @return Configuration[]
|
||||||
|
*/
|
||||||
|
public function getConfiguration(?string $prefix = null): array
|
||||||
|
{
|
||||||
|
return $this->configs;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
tests/Configuration/TimesheetConfigurationTest.php
Normal file
112
tests/Configuration/TimesheetConfigurationTest.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?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\Configuration;
|
||||||
|
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Configuration\TimesheetConfiguration
|
||||||
|
* @covers \App\Configuration\StringAccessibleConfigTrait
|
||||||
|
*/
|
||||||
|
class TimesheetConfigurationTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array $settings
|
||||||
|
* @param array $loaderSettings
|
||||||
|
* @return TimesheetConfiguration
|
||||||
|
*/
|
||||||
|
protected function getSut(array $settings, array $loaderSettings = [])
|
||||||
|
{
|
||||||
|
$loader = new TestConfigLoader($loaderSettings);
|
||||||
|
|
||||||
|
return new TimesheetConfiguration($loader, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'rules' => [
|
||||||
|
'allow_future_times' => false,
|
||||||
|
],
|
||||||
|
'duration_only' => true,
|
||||||
|
'markdown_content' => false,
|
||||||
|
'active_entries' => [
|
||||||
|
'hard_limit' => 99,
|
||||||
|
'soft_limit' => 15,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDefaultLoaderSettings()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new Configuration())->setName('timesheet.rules.allow_future_times')->setValue('1'),
|
||||||
|
(new Configuration())->setName('timesheet.duration_only')->setValue('0'),
|
||||||
|
(new Configuration())->setName('timesheet.markdown_content')->setValue('1'),
|
||||||
|
(new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'),
|
||||||
|
(new Configuration())->setName('timesheet.active_entries.soft_limit')->setValue('3'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPrefix()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals('timesheet', $sut->getPrefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithoutLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals(99, $sut->getActiveEntriesHardLimit());
|
||||||
|
$this->assertEquals(15, $sut->getActiveEntriesSoftLimit());
|
||||||
|
$this->assertEquals(false, $sut->isAllowFutureTimes());
|
||||||
|
$this->assertEquals(true, $sut->isDurationOnly());
|
||||||
|
$this->assertEquals(false, $sut->isMarkdownEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithLoader()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||||
|
$this->assertEquals(7, $sut->getActiveEntriesHardLimit());
|
||||||
|
$this->assertEquals(3, $sut->getActiveEntriesSoftLimit());
|
||||||
|
$this->assertEquals(true, $sut->isAllowFutureTimes());
|
||||||
|
$this->assertEquals(false, $sut->isDurationOnly());
|
||||||
|
$this->assertEquals(true, $sut->isMarkdownEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultWithMixedConfigs()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('timesheet.duration_only')->setValue(''),
|
||||||
|
]);
|
||||||
|
$this->assertEquals(false, $sut->isDurationOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindByKey()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||||
|
$this->assertEquals(false, $sut->find('rules.allow_future_times'));
|
||||||
|
$this->assertEquals(false, $sut->find('timesheet.rules.allow_future_times'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @expectedExceptionMessage Unknown config: foo
|
||||||
|
*/
|
||||||
|
public function testUnknownConfigAreNotImportedAndFindingThemThrowsException()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($this->getDefaultSettings(), [
|
||||||
|
(new Configuration())->setName('timesheet.foo')->setValue('hello'),
|
||||||
|
]);
|
||||||
|
$this->assertEquals('hello', $sut->find('foo'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -271,6 +271,15 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
$this->assertHasFlashSuccess($client, 'Entry was deleted successful');
|
$this->assertHasFlashSuccess($client, 'Entry was deleted successful');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Client $client
|
||||||
|
* @param string|null $message
|
||||||
|
*/
|
||||||
|
protected function assertHasFlashSaveSuccess(Client $client)
|
||||||
|
{
|
||||||
|
$this->assertHasFlashSuccess($client, 'Saved changes successful');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Client $client
|
* @param Client $client
|
||||||
* @param string|null $message
|
* @param string|null $message
|
||||||
@@ -284,6 +293,19 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Client $client
|
||||||
|
* @param string|null $message
|
||||||
|
*/
|
||||||
|
protected function assertHasFlashError(Client $client, string $message = null)
|
||||||
|
{
|
||||||
|
$node = $client->getCrawler()->filter('div.alert.alert-error.alert-dismissible');
|
||||||
|
$this->assertNotEmpty($node->text());
|
||||||
|
if (null !== $message) {
|
||||||
|
$this->assertContains($message, $node->text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Client $client
|
* @param Client $client
|
||||||
* @param string $url
|
* @param string $url
|
||||||
|
|||||||
175
tests/Controller/SystemConfigurationControllerTest.php
Normal file
175
tests/Controller/SystemConfigurationControllerTest.php
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?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\Configuration\SystemConfiguration;
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\Controller\SystemConfigurationController
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class SystemConfigurationControllerTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/admin/system-config/');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/system-config/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIndexAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/system-config/');
|
||||||
|
|
||||||
|
$expectedForms = $this->getTestDataForms();
|
||||||
|
|
||||||
|
$result = $client->getCrawler()->filter('section.content div.box.box-primary');
|
||||||
|
$this->assertEquals(count($expectedForms), count($result));
|
||||||
|
|
||||||
|
$result = $client->getCrawler()->filter('section.content div.box.box-primary form');
|
||||||
|
$this->assertEquals(count($expectedForms), count($result));
|
||||||
|
|
||||||
|
foreach ($expectedForms as $formConfig) {
|
||||||
|
$result = $client->getCrawler()->filter($formConfig[0]);
|
||||||
|
$this->assertEquals(1, count($result));
|
||||||
|
$form = $result->form();
|
||||||
|
$this->assertStringEndsWith($formConfig[1], $form->getUri());
|
||||||
|
$this->assertEquals('POST', $form->getMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTestDataForms()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['#system_configuration_form_timesheet', $this->createUrl('/admin/system-config/timesheet')],
|
||||||
|
['#system_configuration_form_form_customer', $this->createUrl('/admin/system-config/customer')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateTimesheetConfig()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/system-config/');
|
||||||
|
|
||||||
|
$configService = $client->getContainer()->get(SystemConfiguration::class);
|
||||||
|
$this->assertEquals(false, $configService->find('timesheet.markdown_content'));
|
||||||
|
$this->assertEquals(false, $configService->find('timesheet.duration_only'));
|
||||||
|
$this->assertEquals(true, $configService->find('timesheet.rules.allow_future_times'));
|
||||||
|
$this->assertEquals(3, $configService->find('timesheet.active_entries.hard_limit'));
|
||||||
|
$this->assertEquals(1, $configService->find('timesheet.active_entries.soft_limit'));
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('#system_configuration_form_timesheet')->form();
|
||||||
|
$client->submit($form, [
|
||||||
|
'system_configuration_form' => [
|
||||||
|
'configuration' => [
|
||||||
|
['name' => 'timesheet.markdown_content', 'value' => 1],
|
||||||
|
['name' => 'timesheet.duration_only', 'value' => 1],
|
||||||
|
['name' => 'timesheet.rules.allow_future_times', 'value' => false],
|
||||||
|
['name' => 'timesheet.active_entries.hard_limit', 'value' => 99],
|
||||||
|
['name' => 'timesheet.active_entries.soft_limit', 'value' => 77],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/system-config/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSaveSuccess($client);
|
||||||
|
|
||||||
|
$configService = $client->getContainer()->get(SystemConfiguration::class);
|
||||||
|
$this->assertEquals(true, $configService->find('timesheet.markdown_content'));
|
||||||
|
$this->assertEquals(true, $configService->find('timesheet.duration_only'));
|
||||||
|
$this->assertEquals(false, $configService->find('timesheet.rules.allow_future_times'));
|
||||||
|
$this->assertEquals(99, $configService->find('timesheet.active_entries.hard_limit'));
|
||||||
|
$this->assertEquals(77, $configService->find('timesheet.active_entries.soft_limit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateTimesheetConfigValidation()
|
||||||
|
{
|
||||||
|
$this->assertFormHasValidationError(
|
||||||
|
User::ROLE_SUPER_ADMIN,
|
||||||
|
'/admin/system-config/',
|
||||||
|
'#system_configuration_form_timesheet',
|
||||||
|
[
|
||||||
|
'system_configuration_form' => [
|
||||||
|
'configuration' => [
|
||||||
|
['name' => 'timesheet.markdown_content', 'value' => 1],
|
||||||
|
['name' => 'timesheet.duration_only', 'value' => 1],
|
||||||
|
['name' => 'timesheet.rules.allow_future_times', 'value' => 1],
|
||||||
|
['name' => 'timesheet.active_entries.hard_limit', 'value' => -1],
|
||||||
|
['name' => 'timesheet.active_entries.soft_limit', 'value' => -1],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'#system_configuration_form_configuration_3_value',
|
||||||
|
'#system_configuration_form_configuration_4_value',
|
||||||
|
],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateCustomerConfig()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/system-config/');
|
||||||
|
|
||||||
|
$configService = $client->getContainer()->get(SystemConfiguration::class);
|
||||||
|
$this->assertEquals('Europe/Berlin', $configService->find('defaults.customer.timezone'));
|
||||||
|
$this->assertEquals('DE', $configService->find('defaults.customer.country'));
|
||||||
|
$this->assertEquals('EUR', $configService->find('defaults.customer.currency'));
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('#system_configuration_form_form_customer')->form();
|
||||||
|
$client->submit($form, [
|
||||||
|
'system_configuration_form' => [
|
||||||
|
'configuration' => [
|
||||||
|
['name' => 'defaults.customer.timezone', 'value' => 'Atlantic/Canary'],
|
||||||
|
['name' => 'defaults.customer.country', 'value' => 'BB'],
|
||||||
|
['name' => 'defaults.customer.currency', 'value' => 'GBP'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/system-config/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSaveSuccess($client);
|
||||||
|
|
||||||
|
$configService = $client->getContainer()->get(SystemConfiguration::class);
|
||||||
|
$this->assertEquals('Atlantic/Canary', $configService->find('defaults.customer.timezone'));
|
||||||
|
$this->assertEquals('BB', $configService->find('defaults.customer.country'));
|
||||||
|
$this->assertEquals('GBP', $configService->find('defaults.customer.currency'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateCustomerConfigValidation()
|
||||||
|
{
|
||||||
|
$this->assertFormHasValidationError(
|
||||||
|
User::ROLE_SUPER_ADMIN,
|
||||||
|
'/admin/system-config/',
|
||||||
|
'#system_configuration_form_form_customer',
|
||||||
|
[
|
||||||
|
'system_configuration_form' => [
|
||||||
|
'configuration' => [
|
||||||
|
['name' => 'defaults.customer.timezone', 'value' => 'XX'],
|
||||||
|
['name' => 'defaults.customer.country', 'value' => 1],
|
||||||
|
['name' => 'defaults.customer.currency', 'value' => 'XXX'],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'#system_configuration_form_configuration_0_value',
|
||||||
|
'#system_configuration_form_configuration_1_value',
|
||||||
|
'#system_configuration_form_configuration_2_value',
|
||||||
|
],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,7 +187,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasFlashSuccess($client, 'Time recording was started');
|
||||||
|
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
/** @var Timesheet $timesheet */
|
/** @var Timesheet $timesheet */
|
||||||
@@ -198,7 +198,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertEquals(1, $timesheet->getProject()->getId());
|
$this->assertEquals(1, $timesheet->getProject()->getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testStopActionDoesNotShowRateFieldsForUser()
|
public function testCreateActionDoesNotShowRateFieldsForUser()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser();
|
$client = $this->getClientForAuthenticatedUser();
|
||||||
$this->request($client, '/timesheet/create');
|
$this->request($client, '/timesheet/create');
|
||||||
@@ -246,6 +246,40 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$this->assertNull($timesheet->getHourlyRate());
|
$this->assertNull($timesheet->getHourlyRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testStopActionFailsOnStoppedEntry()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->request($client, '/timesheet/create');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||||
|
$client->submit($form, [
|
||||||
|
'timesheet_edit_form' => [
|
||||||
|
'description' => 'Testing is fun!',
|
||||||
|
'fixedRate' => 100,
|
||||||
|
'project' => 1,
|
||||||
|
'activity' => 1,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSuccess($client);
|
||||||
|
|
||||||
|
$this->request($client, '/timesheet/1/stop');
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSuccess($client);
|
||||||
|
|
||||||
|
$this->request($client, '/timesheet/1/stop');
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashError($client, 'Time recording could not be stopped: Timesheet entry already stopped');
|
||||||
|
}
|
||||||
|
|
||||||
public function testCreateActionWithFromAndToValues()
|
public function testCreateActionWithFromAndToValues()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
@@ -336,6 +370,21 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
'Could not find link to documentation'
|
'Could not find link to documentation'
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO more assertions
|
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||||
|
$client->submit($form, [
|
||||||
|
'timesheet_edit_form' => [
|
||||||
|
'description' => 'foo-bar'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSaveSuccess($client);
|
||||||
|
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
/** @var Timesheet $timesheet */
|
||||||
|
$timesheet = $em->getRepository(Timesheet::class)->find(1);
|
||||||
|
$this->assertEquals('foo-bar', $timesheet->getDescription());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
tests/Entity/ConfigurationTest.php
Normal file
35
tests/Entity/ConfigurationTest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\Configuration
|
||||||
|
*/
|
||||||
|
class ConfigurationTest extends AbstractEntityTest
|
||||||
|
{
|
||||||
|
public function testDefaultValues()
|
||||||
|
{
|
||||||
|
$sut = new Configuration();
|
||||||
|
$this->assertNull($sut->getId());
|
||||||
|
$this->assertNull($sut->getName());
|
||||||
|
$this->assertNull($sut->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetterAndGetter()
|
||||||
|
{
|
||||||
|
$sut = new Configuration();
|
||||||
|
$this->assertInstanceOf(Configuration::class, $sut->setName('foo-bar'));
|
||||||
|
$this->assertEquals('foo-bar', $sut->getName());
|
||||||
|
$this->assertInstanceOf(Configuration::class, $sut->setValue('hello world'));
|
||||||
|
$this->assertEquals('hello world', $sut->getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
namespace App\Tests\Twig;
|
namespace App\Tests\Twig;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Twig\MarkdownExtension;
|
use App\Twig\MarkdownExtension;
|
||||||
use App\Utils\Markdown;
|
use App\Utils\Markdown;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
@@ -20,7 +22,9 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function testGetFilters()
|
public function testGetFilters()
|
||||||
{
|
{
|
||||||
$sut = new MarkdownExtension(new Markdown());
|
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
|
||||||
|
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]);
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||||
$filters = $sut->getFilters();
|
$filters = $sut->getFilters();
|
||||||
$this->assertCount(2, $filters);
|
$this->assertCount(2, $filters);
|
||||||
$this->assertEquals('md2html', $filters[0]->getName());
|
$this->assertEquals('md2html', $filters[0]->getName());
|
||||||
@@ -29,14 +33,18 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
|
|
||||||
public function testMarkdownToHtml()
|
public function testMarkdownToHtml()
|
||||||
{
|
{
|
||||||
$sut = new MarkdownExtension(new Markdown());
|
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
|
||||||
|
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]);
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||||
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
$this->assertEquals('<p><em>test</em></p>', $sut->markdownToHtml('*test*'));
|
||||||
$this->assertEquals('<h1 id="foobar">foobar</h1>', $sut->markdownToHtml('# foobar'));
|
$this->assertEquals('<h1 id="foobar">foobar</h1>', $sut->markdownToHtml('# foobar'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTimesheetContent()
|
public function testTimesheetContent()
|
||||||
{
|
{
|
||||||
$sut = new MarkdownExtension(new Markdown(), false);
|
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
|
||||||
|
$config = new TimesheetConfiguration($loader, ['markdown_content' => false]);
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
"- test<br />\n- foo",
|
"- test<br />\n- foo",
|
||||||
$sut->timesheetContent("- test\n- foo")
|
$sut->timesheetContent("- test\n- foo")
|
||||||
@@ -44,7 +52,8 @@ class MarkdownExtensionTest extends TestCase
|
|||||||
$this->assertEquals('', $sut->timesheetContent(null));
|
$this->assertEquals('', $sut->timesheetContent(null));
|
||||||
$this->assertEquals('', $sut->timesheetContent(''));
|
$this->assertEquals('', $sut->timesheetContent(''));
|
||||||
|
|
||||||
$sut = new MarkdownExtension(new Markdown(), true);
|
$config = new TimesheetConfiguration($loader, ['markdown_content' => true]);
|
||||||
|
$sut = new MarkdownExtension(new Markdown(), $config);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
"<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>",
|
"<ul>\n<li>test</li>\n<li>foo</li>\n</ul>\n<p>foo <strong>bar</strong></p>",
|
||||||
$sut->timesheetContent("- test\n- foo\n\nfoo __bar__")
|
$sut->timesheetContent("- test\n- foo\n\nfoo __bar__")
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|||||||
*/
|
*/
|
||||||
class MPdfConverterTest extends KernelTestCase
|
class MPdfConverterTest extends KernelTestCase
|
||||||
{
|
{
|
||||||
function unicode_hex($unicode_dec)
|
public function unicode_hex($unicode_dec)
|
||||||
{
|
{
|
||||||
return (sprintf("%05s", strtoupper(dechex($unicode_dec))));
|
return (sprintf('%05s', strtoupper(dechex($unicode_dec))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test()
|
public function test()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
namespace App\Tests\Validator\Constraints;
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Configuration\ConfigLoaderInterface;
|
||||||
|
use App\Configuration\TimesheetConfiguration;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
@@ -26,14 +28,18 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
|||||||
{
|
{
|
||||||
protected function createValidator($isGranted = true)
|
protected function createValidator($isGranted = true)
|
||||||
{
|
{
|
||||||
$options = [
|
|
||||||
'allow_future_times' => false
|
|
||||||
];
|
|
||||||
|
|
||||||
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
$authMock = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||||
$authMock->method('isGranted')->willReturn($isGranted);
|
$authMock->method('isGranted')->willReturn($isGranted);
|
||||||
|
|
||||||
return new TimesheetValidator($authMock, $options, false);
|
$loader = $this->getMockBuilder(ConfigLoaderInterface::class)->getMock();
|
||||||
|
$config = new TimesheetConfiguration($loader, [
|
||||||
|
'rules' => [
|
||||||
|
'allow_future_times' => false,
|
||||||
|
],
|
||||||
|
'duration_only' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new TimesheetValidator($authMock, $config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<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="about.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="about.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="about.title">
|
<trans-unit id="about.title">
|
||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<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">
|
<file source-language="en" target-language="en" datatype="plaintext" original="about.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="about.title">
|
<trans-unit id="about.title">
|
||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<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="hu" datatype="plaintext" original="about.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="about.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="about.title">
|
<trans-unit id="about.title">
|
||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-11-28T20:00:00Z" source-language="en" target-language="pt-BR" datatype="plaintext" original="about.en.xliff">
|
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="about.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="about.title">
|
<trans-unit id="about.title">
|
||||||
<source>about.title</source>
|
<source>about.title</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2019-02-02T10:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="daterangepicker.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="daterangepicker.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="daterangepicker.apply">
|
<trans-unit id="daterangepicker.apply">
|
||||||
<source>daterangepicker.apply</source>
|
<source>daterangepicker.apply</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2019-02-02T10:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="daterangepicker.en.xliff">
|
<file source-language="en" target-language="en" datatype="plaintext" original="daterangepicker.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="daterangepicker.apply">
|
<trans-unit id="daterangepicker.apply">
|
||||||
<source>daterangepicker.apply</source>
|
<source>daterangepicker.apply</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-28T20:00:00Z" source-language="en" target-language="ar" datatype="plaintext" original="none">
|
<file source-language="en" target-language="ar" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
<file source-language="en" target-language="en" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="es" datatype="plaintext" original="none">
|
<file source-language="en" target-language="es" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="fr" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="fr" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="hu" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="it" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="it" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-11-28T20:00:00Z" source-language="en" target-language="pt-BR" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="exceptions.en.xliff">
|
<file source-language="en" target-language="ru" datatype="plaintext" original="exceptions.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="access.denied">
|
<trans-unit id="access.denied">
|
||||||
<source>access.denied</source>
|
<source>access.denied</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-28T20:00:00Z" source-language="en" target-language="ar" datatype="plaintext" original="none">
|
<file source-language="en" target-language="ar" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
<file source-language="en" target-language="en" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="es" datatype="plaintext" original="none">
|
<file source-language="en" target-language="es" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="fr" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="fr" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="hu" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="it" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="it" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-11-28T20:00:00Z" source-language="en" target-language="pt-BR" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2017-01-12T20:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="flashmessages.en.xliff">
|
<file source-language="en" target-language="ru" datatype="plaintext" original="flashmessages.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-28T20:00:00Z" source-language="en" target-language="ar" datatype="plaintext" original="none">
|
<file source-language="en" target-language="ar" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
<file source-language="en" target-language="en" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="es" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="es" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="fr" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="fr" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="hu" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="it" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="it" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-11-28T20:00:00Z" source-language="en" target-language="pt-BR" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="ru" datatype="plaintext" original="invoice-calculator.en.xliff">
|
<file source-language="en" target-language="ru" datatype="plaintext" original="invoice-calculator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_calculator">
|
<trans-unit id="label.invoice_calculator">
|
||||||
<source>label.invoice_calculator</source>
|
<source>label.invoice_calculator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-28T20:00:00Z" source-language="en" target-language="ar" datatype="plaintext" original="none">
|
<file source-language="en" target-language="ar" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
<file source-language="en" target-language="en" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="es" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="es" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="fr" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="fr" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="hu" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="it" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="it" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-11-28T20:00:00Z" source-language="en" target-language="pt-BR" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="ru" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
<file source-language="en" target-language="ru" datatype="plaintext" original="invoice-numbergenerator.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_number_generator">
|
<trans-unit id="label.invoice_number_generator">
|
||||||
<source>label.invoice_number_generator</source>
|
<source>label.invoice_number_generator</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-28T20:00:00Z" source-language="en" target-language="ar" datatype="plaintext" original="none">
|
<file source-language="en" target-language="ar" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="de" datatype="plaintext" original="invoice-renderer.en.xliff">
|
<file source-language="en" target-language="de" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="en" datatype="plaintext" original="none">
|
<file source-language="en" target-language="en" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="es" datatype="plaintext" original="invoice-renderer.en.xliff">
|
<file source-language="en" target-language="es" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="fr" datatype="plaintext" original="invoice-renderer.en.xliff">
|
<file source-language="en" target-language="fr" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<file date="2018-09-09T16:00:00Z" source-language="en" target-language="hu" datatype="plaintext" original="invoice-renderer.en.xliff">
|
<file source-language="en" target-language="hu" datatype="plaintext" original="invoice-renderer.en.xliff">
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="label.invoice_renderer">
|
<trans-unit id="label.invoice_renderer">
|
||||||
<source>label.invoice_renderer</source>
|
<source>label.invoice_renderer</source>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user