added team permissions (#996)
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
[](https://travis-ci.org/kevinpapst/kimai2)
|
[](https://travis-ci.org/kevinpapst/kimai2)
|
||||||
[](https://codecov.io/gh/kevinpapst/kimai2)
|
[](https://codecov.io/gh/kevinpapst/kimai2)
|
||||||
[](https://gitter.im/kimai2/support)
|
[](https://gitter.im/kimai2/support)
|
||||||
|
[](https://www.bountysource.com/teams/kimai2)
|
||||||
|
|
||||||
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
Kimai is a free, open source and online time-tracking software designed for small businesses and freelancers.
|
||||||
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
|
It is built with modern technologies such as Symfony, Bootstrap, RESTful API, Doctrine, AdminLTE, Webpack, ES6 etc.
|
||||||
|
|||||||
120
composer.lock
generated
120
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "1b34c28fac76bd8fd3aeb4cc048e2f47",
|
"content-hash": "c4acae186589f23fac807bdacdf372c5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "beberlei/doctrineextensions",
|
"name": "beberlei/doctrineextensions",
|
||||||
@@ -2808,16 +2808,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kevinpapst/adminlte-bundle",
|
"name": "kevinpapst/adminlte-bundle",
|
||||||
"version": "2.8.3",
|
"version": "2.8.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
|
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
|
||||||
"reference": "51ebd9e9a6d36885f0e4830527fbbfa46d87955e"
|
"reference": "12c5deaad9276458a2536da281732d70e5255441"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/51ebd9e9a6d36885f0e4830527fbbfa46d87955e",
|
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/12c5deaad9276458a2536da281732d70e5255441",
|
||||||
"reference": "51ebd9e9a6d36885f0e4830527fbbfa46d87955e",
|
"reference": "12c5deaad9276458a2536da281732d70e5255441",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2855,6 +2855,10 @@
|
|||||||
"MIT"
|
"MIT"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Kevin Papst",
|
||||||
|
"homepage": "https://www.kevinpapst.de"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Marc Bach",
|
"name": "Marc Bach",
|
||||||
"homepage": "http://www.avanzu.de"
|
"homepage": "http://www.avanzu.de"
|
||||||
@@ -2862,14 +2866,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Ángel Guzmán Maeso",
|
"name": "Ángel Guzmán Maeso",
|
||||||
"homepage": "https://shakaran.net/blog"
|
"homepage": "https://shakaran.net/blog"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Kevin Papst",
|
|
||||||
"homepage": "https://www.kevinpapst.de"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
|
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
|
||||||
"time": "2019-06-30T16:09:16+00:00"
|
"time": "2019-08-08T08:09:47+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kimai/kimai2-composer",
|
"name": "kimai/kimai2-composer",
|
||||||
@@ -6227,16 +6227,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "82ebae02209c21113908c229e9883c419720738a"
|
"reference": "550ebaac289296ce228a706d0867afc34687e3f4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
|
||||||
"reference": "82ebae02209c21113908c229e9883c419720738a",
|
"reference": "550ebaac289296ce228a706d0867afc34687e3f4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6248,7 +6248,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.11-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6264,13 +6264,13 @@
|
|||||||
"MIT"
|
"MIT"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Gert de Pagter",
|
"name": "Gert de Pagter",
|
||||||
"email": "BackEndTea@gmail.com"
|
"email": "BackEndTea@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Symfony polyfill for ctype functions",
|
"description": "Symfony polyfill for ctype functions",
|
||||||
@@ -6281,25 +6281,25 @@
|
|||||||
"polyfill",
|
"polyfill",
|
||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
"time": "2019-02-06T07:57:58+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-icu",
|
"name": "symfony/polyfill-intl-icu",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-icu.git",
|
"url": "https://github.com/symfony/polyfill-intl-icu.git",
|
||||||
"reference": "999878a3a09d73cae157b0cf89bb6fb2cc073057"
|
"reference": "66810b9d6eb4af54d543867909d65ab9af654d7e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/999878a3a09d73cae157b0cf89bb6fb2cc073057",
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/66810b9d6eb4af54d543867909d65ab9af654d7e",
|
||||||
"reference": "999878a3a09d73cae157b0cf89bb6fb2cc073057",
|
"reference": "66810b9d6eb4af54d543867909d65ab9af654d7e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.3",
|
"php": ">=5.3.3",
|
||||||
"symfony/intl": "~2.3|~3.0|~4.0"
|
"symfony/intl": "~2.3|~3.0|~4.0|~5.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-intl": "For best performance"
|
"ext-intl": "For best performance"
|
||||||
@@ -6307,7 +6307,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.9-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6339,20 +6339,20 @@
|
|||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2019-01-07T19:39:47+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-intl-idn",
|
"name": "symfony/polyfill-intl-idn",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||||
"reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af"
|
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af",
|
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
|
||||||
"reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af",
|
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6366,7 +6366,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.9-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6382,13 +6382,13 @@
|
|||||||
"MIT"
|
"MIT"
|
||||||
],
|
],
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
|
||||||
"name": "Symfony Community",
|
|
||||||
"homepage": "https://symfony.com/contributors"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Laurent Bassin",
|
"name": "Laurent Bassin",
|
||||||
"email": "laurent@bassin.info"
|
"email": "laurent@bassin.info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
|
||||||
@@ -6401,20 +6401,20 @@
|
|||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2019-03-04T13:44:35+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-mbstring",
|
"name": "symfony/polyfill-mbstring",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
|
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
|
||||||
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
|
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6426,7 +6426,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.11-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6460,20 +6460,20 @@
|
|||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2019-02-06T07:57:58+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php72",
|
"name": "symfony/polyfill-php72",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php72.git",
|
"url": "https://github.com/symfony/polyfill-php72.git",
|
||||||
"reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c"
|
"reference": "04ce3335667451138df4307d6a9b61565560199e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c",
|
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
|
||||||
"reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c",
|
"reference": "04ce3335667451138df4307d6a9b61565560199e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6482,7 +6482,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.11-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6515,20 +6515,20 @@
|
|||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2019-02-06T07:57:58+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php73",
|
"name": "symfony/polyfill-php73",
|
||||||
"version": "v1.11.0",
|
"version": "v1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||||
"reference": "d1fb4abcc0c47be136208ad9d68bf59f1ee17abd"
|
"reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/d1fb4abcc0c47be136208ad9d68bf59f1ee17abd",
|
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188",
|
||||||
"reference": "d1fb4abcc0c47be136208ad9d68bf59f1ee17abd",
|
"reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -6537,7 +6537,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "1.11-dev"
|
"dev-master": "1.12-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -6573,7 +6573,7 @@
|
|||||||
"portable",
|
"portable",
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"time": "2019-02-06T07:57:58+00:00"
|
"time": "2019-08-06T08:03:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/profiler-pack",
|
"name": "symfony/profiler-pack",
|
||||||
@@ -9160,16 +9160,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nette/di",
|
"name": "nette/di",
|
||||||
"version": "v3.0.0",
|
"version": "v3.0.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/nette/di.git",
|
"url": "https://github.com/nette/di.git",
|
||||||
"reference": "19d83539245aaacb59470828919182411061841f"
|
"reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/nette/di/zipball/19d83539245aaacb59470828919182411061841f",
|
"url": "https://api.github.com/repos/nette/di/zipball/4aff517a1c6bb5c36fa09733d4cea089f529de6d",
|
||||||
"reference": "19d83539245aaacb59470828919182411061841f",
|
"reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9229,7 +9229,7 @@
|
|||||||
"nette",
|
"nette",
|
||||||
"static"
|
"static"
|
||||||
],
|
],
|
||||||
"time": "2019-04-03T19:35:46+00:00"
|
"time": "2019-08-07T12:11:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "nette/finder",
|
"name": "nette/finder",
|
||||||
|
|||||||
@@ -84,23 +84,33 @@ kimai:
|
|||||||
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
|
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
|
||||||
sets:
|
sets:
|
||||||
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity']
|
ACTIVITIES: ['view_activity','create_activity','edit_activity','budget_activity','delete_activity']
|
||||||
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project']
|
ACTIVITIES_TEAM: ['view_activity','create_activity','edit_teamlead_activity','budget_teamlead_activity']
|
||||||
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer']
|
PROJECTS: ['view_project','create_project','edit_project','budget_project','delete_project','permissions_project']
|
||||||
|
PROJECTS_TEAM: ['view_project','edit_teamlead_project','budget_teamlead_project','permissions_teamlead_project']
|
||||||
|
CUSTOMERS: ['view_customer','create_customer','edit_customer','budget_customer','delete_customer','permissions_customer']
|
||||||
|
CUSTOMERS_TEAM: ['view_customer','edit_teamlead_customer','budget_teamlead_customer']
|
||||||
INVOICE: ['view_invoice','create_invoice']
|
INVOICE: ['view_invoice','create_invoice']
|
||||||
INVOICE_TEMPLATE: ['view_invoice_template','create_invoice_template','edit_invoice_template','delete_invoice_template']
|
INVOICE_TEMPLATE: ['view_invoice_template','create_invoice_template','edit_invoice_template','delete_invoice_template']
|
||||||
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
|
||||||
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
|
||||||
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
|
||||||
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile']
|
PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile','teams_other_profile']
|
||||||
TAGS: ['view_tag','delete_tag']
|
TAGS: ['view_tag','delete_tag']
|
||||||
USER: ['view_user','create_user','delete_user','role_permissions']
|
USER: ['view_user','create_user','delete_user','role_permissions']
|
||||||
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
|
||||||
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
|
RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
|
||||||
EXPORT: ['view_export','create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
EXPORT: ['view_export','create_export','edit_export_own_timesheet','edit_export_other_timesheet']
|
||||||
|
TEAMS: ['view_team','create_team','edit_team','delete_team']
|
||||||
|
# some single default definitions for roles
|
||||||
|
SINGLE_USER: []
|
||||||
|
SINGLE_TEAMLEAD: ['!delete_invoice_template','view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
||||||
|
SINGLE_ADMIN: ['hourly-rate_own_profile','edit_exported_timesheet','teams_own_profile']
|
||||||
|
SINGLE_SUPER_ADMIN: ['hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet','teams_own_profile']
|
||||||
|
# link above sets to one set for each user role
|
||||||
ROLE_USER: ['@TIMESHEET','@PROFILE']
|
ROLE_USER: ['@TIMESHEET','@PROFILE']
|
||||||
ROLE_TEAMLEAD: ['@INVOICE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@EXPORT','@TAGS','@INVOICE_TEMPLATE','!delete_invoice_template','view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
|
ROLE_TEAMLEAD: ['@ACTIVITIES_TEAM','@PROJECTS_TEAM','@CUSTOMERS_TEAM','@TIMESHEET_OTHER','@INVOICE','@TIMESHEET','@PROFILE','@EXPORT','@TAGS','@INVOICE_TEMPLATE','@SINGLE_TEAMLEAD']
|
||||||
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','edit_exported_timesheet']
|
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_ADMIN']
|
||||||
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet']
|
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@TEAMS','@RATE','@RATE_OTHER','@EXPORT','@TAGS','@SINGLE_SUPER_ADMIN']
|
||||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||||
maps:
|
maps:
|
||||||
ROLE_USER: ['ROLE_USER']
|
ROLE_USER: ['ROLE_USER']
|
||||||
@@ -154,18 +164,6 @@ kimai:
|
|||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
# THEME Settings
|
|
||||||
# will be available as twig globals at "kimai_context.*".
|
|
||||||
# See documentation at https://www.kimai.org/documentation/theme.html
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
# theme:
|
|
||||||
# # BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
|
||||||
# # into a searchable and javascript enhanced input type
|
|
||||||
# select_type: ~
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# DASHBOARD
|
# DASHBOARD
|
||||||
# Using the configurable widget sections.
|
# Using the configurable widget sections.
|
||||||
@@ -278,23 +276,6 @@ kimai:
|
|||||||
date: 'd. m. Y'
|
date: 'd. m. Y'
|
||||||
date_time: 'd. m. H:i'
|
date_time: 'd. m. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
# FORMS
|
|
||||||
# Default settings used to populate forms
|
|
||||||
# --------------------------------------------------------------------------------
|
|
||||||
# defaults:
|
|
||||||
# customer:
|
|
||||||
# timezone: Europe/Berlin
|
|
||||||
# country: DE
|
|
||||||
# currency: EUR
|
|
||||||
# user:
|
|
||||||
# timezone: Europe/Berlin
|
|
||||||
# language: de
|
|
||||||
# theme: blue-light
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ nelmio_api_doc:
|
|||||||
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
- { alias: TimesheetSubCollection, type: App\Entity\Timesheet, groups: [Default, Subresource, Timesheet] }
|
||||||
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
- { alias: UserEntity, type: App\Entity\User, groups: [Default, Entity, User] }
|
||||||
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
- { alias: UserCollection, type: App\Entity\User, groups: [Default, Collection, User] }
|
||||||
|
- { alias: TeamEntity, type: App\Entity\Team, groups: [Default, Entity, Team] }
|
||||||
|
- { alias: TeamCollection, type: App\Entity\Team, groups: [Default, Collection, Team] }
|
||||||
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
- { alias: I18nConfig, type: App\API\Model\I18n, groups: [Default] }
|
||||||
areas:
|
areas:
|
||||||
path_patterns:
|
path_patterns:
|
||||||
|
|||||||
14
config/serializer/App/Entity.Team.yml
Normal file
14
config/serializer/App/Entity.Team.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
App\Entity\Team:
|
||||||
|
exclusion_policy: All
|
||||||
|
custom_accessor_order: [id, name, users]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
include: true
|
||||||
|
name:
|
||||||
|
include: true
|
||||||
|
users:
|
||||||
|
exclude: true
|
||||||
|
customers:
|
||||||
|
exclude: true
|
||||||
|
projects:
|
||||||
|
exclude: true
|
||||||
@@ -154,6 +154,11 @@ services:
|
|||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
arguments: ['App\Entity\User']
|
arguments: ['App\Entity\User']
|
||||||
|
|
||||||
|
App\Repository\TeamRepository:
|
||||||
|
class: Doctrine\ORM\EntityRepository
|
||||||
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
|
arguments: ['App\Entity\Team']
|
||||||
|
|
||||||
App\Repository\ActivityRepository:
|
App\Repository\ActivityRepository:
|
||||||
class: Doctrine\ORM\EntityRepository
|
class: Doctrine\ORM\EntityRepository
|
||||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class CustomerController extends BaseApiController
|
|||||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||||
{
|
{
|
||||||
$query = new CustomerQuery();
|
$query = new CustomerQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||||
$query->setOrder($order);
|
$query->setOrder($order);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class ProjectController extends BaseApiController
|
|||||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||||
{
|
{
|
||||||
$query = new ProjectQuery();
|
$query = new ProjectQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||||
$query->setOrder($order);
|
$query->setOrder($order);
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ class TagController extends BaseApiController
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
$this->repository->deleteTag($tag);
|
||||||
$entityManager->remove($tag);
|
|
||||||
$entityManager->flush();
|
|
||||||
|
|
||||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
|||||||
133
src/API/TeamController.php
Normal file
133
src/API/TeamController.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?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 App\API;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Repository\TeamRepository;
|
||||||
|
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||||
|
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||||
|
use FOS\RestBundle\View\View;
|
||||||
|
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Swagger\Annotations as SWG;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @RouteResource("Team")
|
||||||
|
*/
|
||||||
|
class TeamController extends BaseApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TeamRepository
|
||||||
|
*/
|
||||||
|
protected $repository;
|
||||||
|
/**
|
||||||
|
* @var ViewHandlerInterface
|
||||||
|
*/
|
||||||
|
protected $viewHandler;
|
||||||
|
|
||||||
|
public function __construct(ViewHandlerInterface $viewHandler, TeamRepository $repository)
|
||||||
|
{
|
||||||
|
$this->viewHandler = $viewHandler;
|
||||||
|
$this->repository = $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all existing teams
|
||||||
|
*
|
||||||
|
* @SWG\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the collection of all existing teams",
|
||||||
|
* @SWG\Schema(
|
||||||
|
* type="array",
|
||||||
|
* @SWG\Items(ref="#/definitions/TeamCollection")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @Security("is_granted('view_team')")
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||||
|
{
|
||||||
|
$data = $this->repository->findAll();
|
||||||
|
|
||||||
|
$view = new View($data, 200);
|
||||||
|
$view->getContext()->setGroups(['Default', 'Collection', 'Team']);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns one team
|
||||||
|
*
|
||||||
|
* @SWG\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns one team entity",
|
||||||
|
* @SWG\Schema(ref="#/definitions/TeamEntity"),
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function getAction($id)
|
||||||
|
{
|
||||||
|
/** @var Team $data */
|
||||||
|
$data = $this->repository->find($id);
|
||||||
|
|
||||||
|
if (null === $data) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$view = new View($data, 200);
|
||||||
|
$view->getContext()->setGroups(['Default', 'Entity', 'Team']);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a team
|
||||||
|
*
|
||||||
|
* @SWG\Delete(
|
||||||
|
* @SWG\Response(
|
||||||
|
* response=204,
|
||||||
|
* description="Delete one team"
|
||||||
|
* ),
|
||||||
|
* )
|
||||||
|
* @SWG\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* type="integer",
|
||||||
|
* description="Team ID to delete",
|
||||||
|
* required=true,
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @Security("is_granted('delete_team')")
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function deleteAction($id)
|
||||||
|
{
|
||||||
|
$team = $this->repository->find($id);
|
||||||
|
|
||||||
|
if (null === $team) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repository->deleteTeam($team);
|
||||||
|
|
||||||
|
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,4 +47,9 @@ class FormConfiguration implements SystemBundleConfiguration
|
|||||||
{
|
{
|
||||||
return $this->find('user.language');
|
return $this->find('user.language');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUserDefaultCurrency(): string
|
||||||
|
{
|
||||||
|
return $this->find('user.currency');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\FormConfiguration;
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Event\ActivityMetaDefinitionEvent;
|
use App\Event\ActivityMetaDefinitionEvent;
|
||||||
@@ -40,14 +41,19 @@ class ActivityController extends AbstractController
|
|||||||
* @var ActivityRepository
|
* @var ActivityRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var FormConfiguration
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
/**
|
/**
|
||||||
* @var EventDispatcherInterface
|
* @var EventDispatcherInterface
|
||||||
*/
|
*/
|
||||||
protected $dispatcher;
|
protected $dispatcher;
|
||||||
|
|
||||||
public function __construct(ActivityRepository $repository, EventDispatcherInterface $dispatcher)
|
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->configuration = $configuration;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +74,7 @@ class ActivityController extends AbstractController
|
|||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
$query = new ActivityQuery();
|
$query = new ActivityQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
|
|
||||||
$form = $this->getToolbarForm($query);
|
$form = $this->getToolbarForm($query);
|
||||||
@@ -250,15 +257,20 @@ class ActivityController extends AbstractController
|
|||||||
*/
|
*/
|
||||||
private function createEditForm(Activity $activity)
|
private function createEditForm(Activity $activity)
|
||||||
{
|
{
|
||||||
if ($activity->getId() === null) {
|
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||||
$url = $this->generateUrl('admin_activity_create');
|
$url = $this->generateUrl('admin_activity_create');
|
||||||
} else {
|
|
||||||
|
if ($activity->getId() !== null) {
|
||||||
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
$url = $this->generateUrl('admin_activity_edit', ['id' => $activity->getId()]);
|
||||||
|
if (null !== $activity->getProject()) {
|
||||||
|
$currency = $activity->getProject()->getCustomer()->getCurrency();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->createForm(ActivityEditForm::class, $activity, [
|
return $this->createForm(ActivityEditForm::class, $activity, [
|
||||||
'action' => $url,
|
'action' => $url,
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
|
'currency' => $currency,
|
||||||
'create_more' => true,
|
'create_more' => true,
|
||||||
'customer' => true,
|
'customer' => true,
|
||||||
'include_budget' => $this->isGranted('budget', $activity)
|
'include_budget' => $this->isGranted('budget', $activity)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Configuration\FormConfiguration;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Event\CustomerMetaDefinitionEvent;
|
use App\Event\CustomerMetaDefinitionEvent;
|
||||||
use App\Form\CustomerEditForm;
|
use App\Form\CustomerEditForm;
|
||||||
|
use App\Form\CustomerTeamPermissionForm;
|
||||||
use App\Form\Toolbar\CustomerToolbarForm;
|
use App\Form\Toolbar\CustomerToolbarForm;
|
||||||
use App\Form\Type\CustomerType;
|
use App\Form\Type\CustomerType;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
@@ -71,14 +72,11 @@ class CustomerController extends AbstractController
|
|||||||
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
* @Route(path="/", defaults={"page": 1}, name="admin_customer", methods={"GET"})
|
||||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_customer_paginated", methods={"GET"})
|
||||||
* @Security("is_granted('view_customer')")
|
* @Security("is_granted('view_customer')")
|
||||||
*
|
|
||||||
* @param int $page
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
$query = new CustomerQuery();
|
$query = new CustomerQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
|
|
||||||
$form = $this->getToolbarForm($query);
|
$form = $this->getToolbarForm($query);
|
||||||
@@ -98,9 +96,6 @@ class CustomerController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('create_customer')")
|
* @Security("is_granted('create_customer')")
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request)
|
public function createAction(Request $request)
|
||||||
{
|
{
|
||||||
@@ -117,12 +112,39 @@ class CustomerController extends AbstractController
|
|||||||
return $this->renderCustomerForm($customer, $request);
|
return $this->renderCustomerForm($customer, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/{id}/permissions", name="admin_customer_permissions", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('permissions', customer)")
|
||||||
|
*/
|
||||||
|
public function teamPermissions(Customer $customer, Request $request)
|
||||||
|
{
|
||||||
|
$form = $this->createForm(CustomerTeamPermissionForm::class, $customer, [
|
||||||
|
'action' => $this->generateUrl('admin_customer_permissions', ['id' => $customer->getId()]),
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
try {
|
||||||
|
$this->getRepository()->saveCustomer($customer);
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_customer');
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('customer/permissions.html.twig', [
|
||||||
|
'customer' => $customer,
|
||||||
|
'form' => $form->createView()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/budget", name="admin_customer_budget", methods={"GET"})
|
* @Route(path="/{id}/budget", name="admin_customer_budget", methods={"GET"})
|
||||||
* @Security("is_granted('budget', customer)")
|
* @Security("is_granted('budget', customer)")
|
||||||
*
|
|
||||||
* @param Customer $customer
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
public function budgetAction(Customer $customer)
|
public function budgetAction(Customer $customer)
|
||||||
{
|
{
|
||||||
@@ -135,10 +157,6 @@ class CustomerController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
* @Route(path="/{id}/edit", name="admin_customer_edit", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('edit', customer)")
|
* @Security("is_granted('edit', customer)")
|
||||||
*
|
|
||||||
* @param Customer $customer
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function editAction(Customer $customer, Request $request)
|
public function editAction(Customer $customer, Request $request)
|
||||||
{
|
{
|
||||||
@@ -148,10 +166,6 @@ class CustomerController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
* @Route(path="/{id}/delete", name="admin_customer_delete", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('delete', customer)")
|
* @Security("is_granted('delete', customer)")
|
||||||
*
|
|
||||||
* @param Customer $customer
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function deleteAction(Customer $customer, Request $request)
|
public function deleteAction(Customer $customer, Request $request)
|
||||||
{
|
{
|
||||||
@@ -169,6 +183,7 @@ class CustomerController extends AbstractController
|
|||||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
||||||
$query = new CustomerFormTypeQuery();
|
$query = new CustomerFormTypeQuery();
|
||||||
$query->setCustomerToIgnore($customer);
|
$query->setCustomerToIgnore($customer);
|
||||||
|
$query->setUser($this->getUser());
|
||||||
|
|
||||||
return $repo->getQueryBuilderForFormType($query);
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
@@ -229,11 +244,7 @@ class CustomerController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function getToolbarForm(CustomerQuery $query): FormInterface
|
||||||
* @param CustomerQuery $query
|
|
||||||
* @return FormInterface
|
|
||||||
*/
|
|
||||||
protected function getToolbarForm(CustomerQuery $query)
|
|
||||||
{
|
{
|
||||||
return $this->createForm(CustomerToolbarForm::class, $query, [
|
return $this->createForm(CustomerToolbarForm::class, $query, [
|
||||||
'action' => $this->generateUrl('admin_customer', [
|
'action' => $this->generateUrl('admin_customer', [
|
||||||
@@ -243,11 +254,7 @@ class CustomerController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createEditForm(Customer $customer): FormInterface
|
||||||
* @param Customer $customer
|
|
||||||
* @return FormInterface
|
|
||||||
*/
|
|
||||||
private function createEditForm(Customer $customer)
|
|
||||||
{
|
{
|
||||||
if ($customer->getId() === null) {
|
if ($customer->getId() === null) {
|
||||||
$url = $this->generateUrl('admin_customer_create');
|
$url = $this->generateUrl('admin_customer_create');
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class ExportController extends AbstractController
|
|||||||
$query->setEnd($end);
|
$query->setEnd($end);
|
||||||
$query->setState(ExportQuery::STATE_STOPPED);
|
$query->setState(ExportQuery::STATE_STOPPED);
|
||||||
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class InvoiceController extends AbstractController
|
|||||||
$query->setBegin($begin);
|
$query->setBegin($begin);
|
||||||
$query->setEnd($end);
|
$query->setEnd($end);
|
||||||
$query->setState(InvoiceQuery::STATE_STOPPED);
|
$query->setState(InvoiceQuery::STATE_STOPPED);
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ use App\Form\UserEditType;
|
|||||||
use App\Form\UserPasswordType;
|
use App\Form\UserPasswordType;
|
||||||
use App\Form\UserPreferencesForm;
|
use App\Form\UserPreferencesForm;
|
||||||
use App\Form\UserRolesType;
|
use App\Form\UserRolesType;
|
||||||
|
use App\Form\UserTeamsType;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use App\Voter\UserVoter;
|
use App\Voter\UserVoter;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Form\Form;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
|
||||||
|
|
||||||
@@ -175,6 +177,28 @@ class ProfileController extends AbstractController
|
|||||||
return $this->getProfileView($profile, 'roles', null, null, $form);
|
return $this->getProfileView($profile, 'roles', null, null, $form);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/{username}/teams", name="user_profile_teams", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('teams', profile)")
|
||||||
|
*/
|
||||||
|
public function teamsAction(User $profile, Request $request)
|
||||||
|
{
|
||||||
|
$form = $this->createTeamsForm($profile);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$entityManager = $this->getDoctrine()->getManager();
|
||||||
|
$entityManager->persist($profile);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('user_profile_teams', ['username' => $profile->getUsername()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getProfileView($profile, 'teams', null, null, null, null, $form);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('preferences', profile)")
|
* @Security("is_granted('preferences', profile)")
|
||||||
@@ -236,24 +260,15 @@ class ProfileController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param User $user
|
|
||||||
* @param string $tab
|
|
||||||
* @param Form|null $editForm
|
|
||||||
* @param Form|null $pwdForm
|
|
||||||
* @param Form|null $rolesForm
|
|
||||||
* @param Form|null $apiTokenForm
|
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
|
||||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
|
||||||
*/
|
|
||||||
protected function getProfileView(
|
protected function getProfileView(
|
||||||
User $user,
|
User $user,
|
||||||
string $tab,
|
string $tab,
|
||||||
Form $editForm = null,
|
FormInterface $editForm = null,
|
||||||
Form $pwdForm = null,
|
FormInterface $pwdForm = null,
|
||||||
Form $rolesForm = null,
|
FormInterface $rolesForm = null,
|
||||||
Form $apiTokenForm = null
|
FormInterface $apiTokenForm = null,
|
||||||
) {
|
FormInterface $teamsForm = null
|
||||||
|
): Response {
|
||||||
$forms = [];
|
$forms = [];
|
||||||
|
|
||||||
if ($this->isGranted(UserVoter::EDIT, $user)) {
|
if ($this->isGranted(UserVoter::EDIT, $user)) {
|
||||||
@@ -268,6 +283,10 @@ class ProfileController extends AbstractController
|
|||||||
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
|
||||||
$forms['api-token'] = $apiTokenForm->createView();
|
$forms['api-token'] = $apiTokenForm->createView();
|
||||||
}
|
}
|
||||||
|
if ($this->isGranted(UserVoter::TEAMS, $user)) {
|
||||||
|
$teamsForm = $teamsForm ?: $this->createTeamsForm($user);
|
||||||
|
$forms['teams'] = $teamsForm->createView();
|
||||||
|
}
|
||||||
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
if ($this->isGranted(UserVoter::ROLES, $user)) {
|
||||||
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
|
||||||
$forms['roles'] = $rolesForm->createView();
|
$forms['roles'] = $rolesForm->createView();
|
||||||
@@ -280,11 +299,7 @@ class ProfileController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createPreferencesForm(User $user): FormInterface
|
||||||
* @param User $user
|
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
|
||||||
*/
|
|
||||||
private function createPreferencesForm(User $user)
|
|
||||||
{
|
{
|
||||||
// we need to prepare the user preferences, which is done via an EventSubscriber
|
// we need to prepare the user preferences, which is done via an EventSubscriber
|
||||||
$event = new PrepareUserEvent($user);
|
$event = new PrepareUserEvent($user);
|
||||||
@@ -300,11 +315,7 @@ class ProfileController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createEditForm(User $user): FormInterface
|
||||||
* @param User $user
|
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
|
||||||
*/
|
|
||||||
private function createEditForm(User $user)
|
|
||||||
{
|
{
|
||||||
return $this->createForm(
|
return $this->createForm(
|
||||||
UserEditType::class,
|
UserEditType::class,
|
||||||
@@ -317,11 +328,7 @@ class ProfileController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createRolesForm(User $user): FormInterface
|
||||||
* @param User $user
|
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
|
||||||
*/
|
|
||||||
private function createRolesForm(User $user)
|
|
||||||
{
|
{
|
||||||
return $this->createForm(
|
return $this->createForm(
|
||||||
UserRolesType::class,
|
UserRolesType::class,
|
||||||
@@ -333,11 +340,19 @@ class ProfileController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createTeamsForm(User $user): FormInterface
|
||||||
* @param User $user
|
{
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
return $this->createForm(
|
||||||
*/
|
UserTeamsType::class,
|
||||||
private function createPasswordForm(User $user)
|
$user,
|
||||||
|
[
|
||||||
|
'action' => $this->generateUrl('user_profile_teams', ['username' => $user->getUsername()]),
|
||||||
|
'method' => 'POST',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createPasswordForm(User $user): FormInterface
|
||||||
{
|
{
|
||||||
return $this->createForm(
|
return $this->createForm(
|
||||||
UserPasswordType::class,
|
UserPasswordType::class,
|
||||||
@@ -350,11 +365,7 @@ class ProfileController extends AbstractController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createApiTokenForm(User $user): FormInterface
|
||||||
* @param User $user
|
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
|
||||||
*/
|
|
||||||
private function createApiTokenForm(User $user)
|
|
||||||
{
|
{
|
||||||
return $this->createForm(
|
return $this->createForm(
|
||||||
UserApiTokenType::class,
|
UserApiTokenType::class,
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Configuration\FormConfiguration;
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Event\ProjectMetaDefinitionEvent;
|
use App\Event\ProjectMetaDefinitionEvent;
|
||||||
use App\Form\ProjectEditForm;
|
use App\Form\ProjectEditForm;
|
||||||
|
use App\Form\ProjectTeamPermissionForm;
|
||||||
use App\Form\Toolbar\ProjectToolbarForm;
|
use App\Form\Toolbar\ProjectToolbarForm;
|
||||||
use App\Form\Type\ProjectType;
|
use App\Form\Type\ProjectType;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
@@ -40,14 +42,19 @@ class ProjectController extends AbstractController
|
|||||||
* @var ProjectRepository
|
* @var ProjectRepository
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var FormConfiguration
|
||||||
|
*/
|
||||||
|
private $configuration;
|
||||||
/**
|
/**
|
||||||
* @var EventDispatcherInterface
|
* @var EventDispatcherInterface
|
||||||
*/
|
*/
|
||||||
protected $dispatcher;
|
protected $dispatcher;
|
||||||
|
|
||||||
public function __construct(ProjectRepository $repository, EventDispatcherInterface $dispatcher)
|
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->configuration = $configuration;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +67,11 @@ class ProjectController extends AbstractController
|
|||||||
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
* @Route(path="/", defaults={"page": 1}, name="admin_project", methods={"GET"})
|
||||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_project_paginated", methods={"GET"})
|
||||||
* @Security("is_granted('view_project')")
|
* @Security("is_granted('view_project')")
|
||||||
*
|
|
||||||
* @param int $page
|
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
public function indexAction($page, Request $request)
|
public function indexAction($page, Request $request)
|
||||||
{
|
{
|
||||||
$query = new ProjectQuery();
|
$query = new ProjectQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
|
|
||||||
$form = $this->getToolbarForm($query);
|
$form = $this->getToolbarForm($query);
|
||||||
@@ -85,14 +89,40 @@ class ProjectController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/{id}/permissions", name="admin_project_permissions", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('permissions', project)")
|
||||||
|
*/
|
||||||
|
public function teamPermissions(Project $project, Request $request)
|
||||||
|
{
|
||||||
|
$form = $this->createForm(ProjectTeamPermissionForm::class, $project, [
|
||||||
|
'action' => $this->generateUrl('admin_project_permissions', ['id' => $project->getId()]),
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
try {
|
||||||
|
$this->getRepository()->saveProject($project);
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_project');
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('project/permissions.html.twig', [
|
||||||
|
'project' => $project,
|
||||||
|
'form' => $form->createView()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
* @Route(path="/create", name="admin_project_create", methods={"GET", "POST"})
|
||||||
* @Route(path="/create/{customer}", name="admin_project_create_with_customer", methods={"GET", "POST"})
|
* @Route(path="/create/{customer}", name="admin_project_create_with_customer", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('create_project')")
|
* @Security("is_granted('create_project')")
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Customer|null $customer
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request, ?Customer $customer = null)
|
public function createAction(Request $request, ?Customer $customer = null)
|
||||||
{
|
{
|
||||||
@@ -108,9 +138,6 @@ class ProjectController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/budget", name="admin_project_budget", methods={"GET"})
|
* @Route(path="/{id}/budget", name="admin_project_budget", methods={"GET"})
|
||||||
* @Security("is_granted('budget', project)")
|
* @Security("is_granted('budget', project)")
|
||||||
*
|
|
||||||
* @param Project $project
|
|
||||||
* @return Response
|
|
||||||
*/
|
*/
|
||||||
public function budgetAction(Project $project)
|
public function budgetAction(Project $project)
|
||||||
{
|
{
|
||||||
@@ -123,10 +150,6 @@ class ProjectController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
* @Route(path="/{id}/edit", name="admin_project_edit", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('edit', project)")
|
* @Security("is_granted('edit', project)")
|
||||||
*
|
|
||||||
* @param Project $project
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function editAction(Project $project, Request $request)
|
public function editAction(Project $project, Request $request)
|
||||||
{
|
{
|
||||||
@@ -136,10 +159,6 @@ class ProjectController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
* @Route(path="/{id}/delete", name="admin_project_delete", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('delete', project)")
|
* @Security("is_granted('delete', project)")
|
||||||
*
|
|
||||||
* @param Project $project
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Response
|
|
||||||
*/
|
*/
|
||||||
public function deleteAction(Project $project, Request $request)
|
public function deleteAction(Project $project, Request $request)
|
||||||
{
|
{
|
||||||
@@ -158,6 +177,7 @@ class ProjectController extends AbstractController
|
|||||||
$query = new ProjectFormTypeQuery();
|
$query = new ProjectFormTypeQuery();
|
||||||
$query->setCustomer($project->getCustomer());
|
$query->setCustomer($project->getCustomer());
|
||||||
$query->setProjectToIgnore($project);
|
$query->setProjectToIgnore($project);
|
||||||
|
$query->setUser($this->getUser());
|
||||||
|
|
||||||
return $repo->getQueryBuilderForFormType($query);
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
@@ -225,11 +245,7 @@ class ProjectController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function getToolbarForm(ProjectQuery $query): FormInterface
|
||||||
* @param ProjectQuery $query
|
|
||||||
* @return FormInterface
|
|
||||||
*/
|
|
||||||
protected function getToolbarForm(ProjectQuery $query)
|
|
||||||
{
|
{
|
||||||
return $this->createForm(ProjectToolbarForm::class, $query, [
|
return $this->createForm(ProjectToolbarForm::class, $query, [
|
||||||
'action' => $this->generateUrl('admin_project', [
|
'action' => $this->generateUrl('admin_project', [
|
||||||
@@ -239,16 +255,12 @@ class ProjectController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function createEditForm(Project $project): FormInterface
|
||||||
* @param Project $project
|
|
||||||
* @return FormInterface
|
|
||||||
*/
|
|
||||||
private function createEditForm(Project $project)
|
|
||||||
{
|
{
|
||||||
if ($project->getId() === null) {
|
$currency = $this->configuration->getCustomerDefaultCurrency();
|
||||||
$url = $this->generateUrl('admin_project_create');
|
$url = $this->generateUrl('admin_project_create');
|
||||||
$currency = Customer::DEFAULT_CURRENCY;
|
|
||||||
} else {
|
if ($project->getId() !== null) {
|
||||||
$url = $this->generateUrl('admin_project_edit', ['id' => $project->getId()]);
|
$url = $this->generateUrl('admin_project_edit', ['id' => $project->getId()]);
|
||||||
$currency = $project->getCustomer()->getCurrency();
|
$currency = $project->getCustomer()->getCurrency();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ class SystemConfigurationController extends AbstractController
|
|||||||
->setName('defaults.user.theme')
|
->setName('defaults.user.theme')
|
||||||
->setLabel('skin')
|
->setLabel('skin')
|
||||||
->setType(SkinType::class),
|
->setType(SkinType::class),
|
||||||
|
(new Configuration())
|
||||||
|
->setName('defaults.user.currency')
|
||||||
|
->setLabel('currency')
|
||||||
|
->setType(CurrencyType::class),
|
||||||
]),
|
]),
|
||||||
(new SystemConfigurationModel())
|
(new SystemConfigurationModel())
|
||||||
->setSection(SystemConfigurationModel::SECTION_THEME)
|
->setSection(SystemConfigurationModel::SECTION_THEME)
|
||||||
|
|||||||
185
src/Controller/TeamController.php
Normal file
185
src/Controller/TeamController.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?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\Entity\Team;
|
||||||
|
use App\Form\TeamCustomerForm;
|
||||||
|
use App\Form\TeamEditForm;
|
||||||
|
use App\Form\TeamProjectForm;
|
||||||
|
use App\Form\Toolbar\TeamToolbarForm;
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use App\Repository\TeamRepository;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/admin/teams")
|
||||||
|
* @Security("is_granted('view_team')")
|
||||||
|
*/
|
||||||
|
class TeamController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TeamRepository
|
||||||
|
*/
|
||||||
|
private $repository;
|
||||||
|
|
||||||
|
public function __construct(TeamRepository $repository)
|
||||||
|
{
|
||||||
|
$this->repository = $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/", defaults={"page": 1}, name="admin_team", methods={"GET"})
|
||||||
|
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_team_paginated", methods={"GET"})
|
||||||
|
*
|
||||||
|
* @param TeamRepository $repository
|
||||||
|
* @param Request $request
|
||||||
|
* @param int $page
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function listTeams(TeamRepository $repository, Request $request, $page)
|
||||||
|
{
|
||||||
|
$query = new TeamQuery();
|
||||||
|
$query->setPage($page);
|
||||||
|
$query->setOrderBy('name');
|
||||||
|
|
||||||
|
$form = $this->getToolbarForm($query);
|
||||||
|
$form->setData($query);
|
||||||
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
|
$teams = $repository->getPagerfantaForQuery($query);
|
||||||
|
|
||||||
|
return $this->render('team/index.html.twig', [
|
||||||
|
'teams' => $teams,
|
||||||
|
'query' => $query,
|
||||||
|
'showFilter' => $query->isDirty(),
|
||||||
|
'toolbarForm' => $form->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/create", name="admin_team_create", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('create_team')")
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return RedirectResponse|Response
|
||||||
|
*/
|
||||||
|
public function createTeam(Request $request)
|
||||||
|
{
|
||||||
|
return $this->renderEditScreen(new Team(), $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/{id}/edit", name="admin_team_edit", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('edit', team)")
|
||||||
|
*/
|
||||||
|
public function editAction(Team $team, Request $request)
|
||||||
|
{
|
||||||
|
return $this->renderEditScreen($team, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderEditScreen(Team $team, Request $request): Response
|
||||||
|
{
|
||||||
|
$customerForm = null;
|
||||||
|
$projectForm = null;
|
||||||
|
|
||||||
|
if ($team->getId() === null) {
|
||||||
|
$url = $this->generateUrl('admin_team_create');
|
||||||
|
} else {
|
||||||
|
$url = $this->generateUrl('admin_team_edit', ['id' => $team->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$editForm = $this->createForm(TeamEditForm::class, $team, [
|
||||||
|
'action' => $url,
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && (null !== ($editFormValues = $request->get($editForm->getName())))) {
|
||||||
|
$editForm->submit($editFormValues, true);
|
||||||
|
|
||||||
|
if ($editForm->isValid()) {
|
||||||
|
try {
|
||||||
|
// make sure that the teamlead is always part of the team, otherwise permission checks
|
||||||
|
// and filtering might not work as expected!
|
||||||
|
$team->addUser($team->getTeamLead());
|
||||||
|
|
||||||
|
$this->repository->saveTeam($team);
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $team->getId()) {
|
||||||
|
$customerForm = $this->createForm(TeamCustomerForm::class, $team, [
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && (null !== ($customerFormValues = $request->get($customerForm->getName())))) {
|
||||||
|
$customerForm->submit($customerFormValues, true);
|
||||||
|
|
||||||
|
if ($customerForm->isValid()) {
|
||||||
|
try {
|
||||||
|
$this->repository->saveTeam($team);
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectForm = $this->createForm(TeamProjectForm::class, $team, [
|
||||||
|
'method' => 'POST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->isMethod('POST') && (null !== ($projectFormValues = $request->get($projectForm->getName())))) {
|
||||||
|
$projectForm->submit($projectFormValues, true);
|
||||||
|
|
||||||
|
if ($projectForm->isValid()) {
|
||||||
|
try {
|
||||||
|
$this->repository->saveTeam($team);
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('admin_team_edit', ['id' => $team->getId()]);
|
||||||
|
} catch (ORMException $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('team/edit.html.twig', [
|
||||||
|
'team' => $team,
|
||||||
|
'form' => $editForm->createView(),
|
||||||
|
'customerForm' => $customerForm ? $customerForm->createView() : null,
|
||||||
|
'projectForm' => $projectForm ? $projectForm->createView() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getToolbarForm(TeamQuery $query): FormInterface
|
||||||
|
{
|
||||||
|
return $this->createForm(TeamToolbarForm::class, $query, [
|
||||||
|
'action' => $this->generateUrl('admin_team', [
|
||||||
|
'page' => $query->getPage(),
|
||||||
|
]),
|
||||||
|
'method' => 'GET',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,9 +112,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
|
|
||||||
$dirtyQuery = $query->isDirty();
|
$dirtyQuery = $query->isDirty();
|
||||||
|
|
||||||
if (!$this->includeUserInForms()) {
|
$this->prepareQuery($query);
|
||||||
$query->setUser($this->getUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
$pager = $this->getRepository()->getPagerfantaForQuery($query);
|
$pager = $this->getRepository()->getPagerfantaForQuery($query);
|
||||||
|
|
||||||
@@ -253,9 +251,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
}
|
}
|
||||||
$query->getEnd()->setTime(23, 59, 59);
|
$query->getEnd()->setTime(23, 59, 59);
|
||||||
|
|
||||||
if (!$this->includeUserInForms()) {
|
$this->prepareQuery($query);
|
||||||
$query->setUser($this->getUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
$entries = $this->getRepository()->getTimesheetsForQuery($query);
|
$entries = $this->getRepository()->getTimesheetsForQuery($query);
|
||||||
|
|
||||||
@@ -265,6 +261,11 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareQuery(TimesheetQuery $query)
|
||||||
|
{
|
||||||
|
$query->setUser($this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
protected function getCreateForm(Timesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||||
{
|
{
|
||||||
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
return $this->createForm($this->getCreateFormClassName(), $entry, [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Timesheet;
|
|||||||
use App\Form\TimesheetAdminEditForm;
|
use App\Form\TimesheetAdminEditForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TagRepository;
|
use App\Repository\TagRepository;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -76,6 +77,11 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareQuery(TimesheetQuery $query)
|
||||||
|
{
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
protected function getCreateFormClassName(): string
|
protected function getCreateFormClassName(): string
|
||||||
{
|
{
|
||||||
return TimesheetAdminEditForm::class;
|
return TimesheetAdminEditForm::class;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\DependencyInjection;
|
namespace App\DependencyInjection;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Timesheet\Rounding\RoundingInterface;
|
use App\Timesheet\Rounding\RoundingInterface;
|
||||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||||
@@ -439,7 +440,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->children()
|
->children()
|
||||||
->scalarNode('timezone')->defaultNull()->end()
|
->scalarNode('timezone')->defaultNull()->end()
|
||||||
->scalarNode('country')->defaultValue('DE')->end()
|
->scalarNode('country')->defaultValue('DE')->end()
|
||||||
->scalarNode('currency')->defaultValue('EUR')->end()
|
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->arrayNode('user')
|
->arrayNode('user')
|
||||||
@@ -448,9 +449,9 @@ class Configuration implements ConfigurationInterface
|
|||||||
->scalarNode('timezone')->defaultNull()->end()
|
->scalarNode('timezone')->defaultNull()->end()
|
||||||
->scalarNode('language')->defaultValue(User::DEFAULT_LANGUAGE)->end()
|
->scalarNode('language')->defaultValue(User::DEFAULT_LANGUAGE)->end()
|
||||||
->scalarNode('theme')->defaultNull()->end()
|
->scalarNode('theme')->defaultNull()->end()
|
||||||
|
->scalarNode('currency')->defaultValue(Customer::DEFAULT_CURRENCY)->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -160,9 +160,26 @@ class Customer implements EntityWithMetaFields
|
|||||||
*/
|
*/
|
||||||
private $meta;
|
private $meta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Team[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="Team", cascade={"remove", "persist"}, inversedBy="customers")
|
||||||
|
* @ORM\JoinTable(
|
||||||
|
* name="kimai2_customers_teams",
|
||||||
|
* joinColumns={
|
||||||
|
* @ORM\JoinColumn(name="customer_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* },
|
||||||
|
* inverseJoinColumns={
|
||||||
|
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $teams;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->meta = new ArrayCollection();
|
$this->meta = new ArrayCollection();
|
||||||
|
$this->teams = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -399,6 +416,33 @@ class Customer implements EntityWithMetaFields
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team)
|
||||||
|
{
|
||||||
|
if ($this->teams->contains($team)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->teams->add($team);
|
||||||
|
$team->addCustomer($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeTeam(Team $team)
|
||||||
|
{
|
||||||
|
if (!$this->teams->contains($team)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->teams->removeElement($team);
|
||||||
|
$team->removeCustomer($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Team>
|
||||||
|
*/
|
||||||
|
public function getTeams(): Collection
|
||||||
|
{
|
||||||
|
return $this->teams;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -90,9 +90,26 @@ class Project implements EntityWithMetaFields
|
|||||||
*/
|
*/
|
||||||
private $meta;
|
private $meta;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Team[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="Team", cascade={"remove", "persist"}, inversedBy="projects")
|
||||||
|
* @ORM\JoinTable(
|
||||||
|
* name="kimai2_projects_teams",
|
||||||
|
* joinColumns={
|
||||||
|
* @ORM\JoinColumn(name="project_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* },
|
||||||
|
* inverseJoinColumns={
|
||||||
|
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $teams;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->meta = new ArrayCollection();
|
$this->meta = new ArrayCollection();
|
||||||
|
$this->teams = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -223,6 +240,33 @@ class Project implements EntityWithMetaFields
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team)
|
||||||
|
{
|
||||||
|
if ($this->teams->contains($team)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->teams->add($team);
|
||||||
|
$team->addProject($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeTeam(Team $team)
|
||||||
|
{
|
||||||
|
if (!$this->teams->contains($team)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->teams->removeElement($team);
|
||||||
|
$team->removeProject($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Team>
|
||||||
|
*/
|
||||||
|
public function getTeams(): Collection
|
||||||
|
{
|
||||||
|
return $this->teams;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
210
src/Entity/Team.php
Normal file
210
src/Entity/Team.php
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
<?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\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Table(name="kimai2_teams",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* @ORM\Entity(repositoryClass="App\Repository\TeamRepository")
|
||||||
|
* @UniqueEntity("name")
|
||||||
|
*/
|
||||||
|
class Team
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="id", type="integer")
|
||||||
|
* @ORM\Id
|
||||||
|
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||||
|
*/
|
||||||
|
private $id;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||||
|
* @Assert\NotBlank()
|
||||||
|
* @Assert\Length(min=2, max=100)
|
||||||
|
*/
|
||||||
|
private $name;
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $teamlead;
|
||||||
|
/**
|
||||||
|
* @var User[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="User", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||||
|
*/
|
||||||
|
private $users;
|
||||||
|
/**
|
||||||
|
* @var Customer[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||||
|
*/
|
||||||
|
private $customers;
|
||||||
|
/**
|
||||||
|
* @var Project[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||||
|
*/
|
||||||
|
private $projects;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->users = new ArrayCollection();
|
||||||
|
$this->customers = new ArrayCollection();
|
||||||
|
$this->projects = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): Team
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): ?string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTeamLead(): ?User
|
||||||
|
{
|
||||||
|
return $this->teamlead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTeamlead(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->teamlead === $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTeamLead(User $teamlead): Team
|
||||||
|
{
|
||||||
|
$this->teamlead = $teamlead;
|
||||||
|
$this->addUser($teamlead);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasUser(User $user): bool
|
||||||
|
{
|
||||||
|
return $this->users->contains($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUser(User $user)
|
||||||
|
{
|
||||||
|
if ($this->users->contains($user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->users->add($user);
|
||||||
|
$user->addTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUser(User $user)
|
||||||
|
{
|
||||||
|
if (!$this->users->contains($user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->users->removeElement($user);
|
||||||
|
$user->removeTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<User>
|
||||||
|
*/
|
||||||
|
public function getUsers(): iterable
|
||||||
|
{
|
||||||
|
return $this->users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addCustomer(Customer $customer)
|
||||||
|
{
|
||||||
|
if ($this->customers->contains($customer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->customers->add($customer);
|
||||||
|
$customer->addTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeCustomer(Customer $customer)
|
||||||
|
{
|
||||||
|
if (!$this->customers->contains($customer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->customers->removeElement($customer);
|
||||||
|
$customer->removeTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Customer>
|
||||||
|
*/
|
||||||
|
public function getCustomers(): iterable
|
||||||
|
{
|
||||||
|
return $this->customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addProject(Project $project)
|
||||||
|
{
|
||||||
|
if ($this->projects->contains($project)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->projects->add($project);
|
||||||
|
$project->addTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeProject(Project $project)
|
||||||
|
{
|
||||||
|
if (!$this->projects->contains($project)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->projects->removeElement($project);
|
||||||
|
$project->removeTeam($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Project>
|
||||||
|
*/
|
||||||
|
public function getProjects(): iterable
|
||||||
|
{
|
||||||
|
return $this->projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -140,14 +140,14 @@ class Timesheet implements EntityWithMetaFields
|
|||||||
/**
|
/**
|
||||||
* @var Tag[]|ArrayCollection
|
* @var Tag[]|ArrayCollection
|
||||||
*
|
*
|
||||||
* @ORM\ManyToMany(targetEntity="Tag", inversedBy="timesheets", cascade={"persist"})
|
* @ORM\ManyToMany(targetEntity="Tag", inversedBy="timesheets", cascade={"remove", "persist"})
|
||||||
* @ORM\JoinTable(
|
* @ORM\JoinTable(
|
||||||
* name="kimai2_timesheet_tags",
|
* name="kimai2_timesheet_tags",
|
||||||
* joinColumns={
|
* joinColumns={
|
||||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id")
|
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
* },
|
* },
|
||||||
* inverseJoinColumns={
|
* inverseJoinColumns={
|
||||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -95,6 +95,22 @@ class User extends BaseUser implements UserInterface
|
|||||||
*/
|
*/
|
||||||
private $preferences;
|
private $preferences;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Team[]|ArrayCollection
|
||||||
|
*
|
||||||
|
* @ORM\ManyToMany(targetEntity="Team", inversedBy="users", cascade={"remove", "persist"})
|
||||||
|
* @ORM\JoinTable(
|
||||||
|
* name="kimai2_users_teams",
|
||||||
|
* joinColumns={
|
||||||
|
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* },
|
||||||
|
* inverseJoinColumns={
|
||||||
|
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
private $teams;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User constructor.
|
* User constructor.
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +119,7 @@ class User extends BaseUser implements UserInterface
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->registeredAt = new \DateTime();
|
$this->registeredAt = new \DateTime();
|
||||||
$this->preferences = new ArrayCollection();
|
$this->preferences = new ArrayCollection();
|
||||||
|
$this->teams = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -274,6 +291,55 @@ class User extends BaseUser implements UserInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team): User
|
||||||
|
{
|
||||||
|
if ($this->teams->contains($team)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->teams->add($team);
|
||||||
|
$team->addUser($this);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeTeam(Team $team)
|
||||||
|
{
|
||||||
|
if (!$this->teams->contains($team)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->teams->removeElement($team);
|
||||||
|
$team->removeUser($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Team>
|
||||||
|
*/
|
||||||
|
public function getTeams(): Collection
|
||||||
|
{
|
||||||
|
return $this->teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInTeam(Team $team): bool
|
||||||
|
{
|
||||||
|
return $this->teams->contains($team);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTeamleadOf(Team $team): bool
|
||||||
|
{
|
||||||
|
return $team->getTeamLead() === $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTeamlead(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(static::ROLE_TEAMLEAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(static::ROLE_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class UserPreference
|
|||||||
* @ORM\Column(name="id", type="integer")
|
* @ORM\Column(name="id", type="integer")
|
||||||
*/
|
*/
|
||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var User
|
* @var User
|
||||||
*
|
*
|
||||||
@@ -47,7 +46,6 @@ class UserPreference
|
|||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
private $user;
|
private $user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
@@ -55,28 +53,29 @@ class UserPreference
|
|||||||
* @Assert\Length(min=2, max=50)
|
* @Assert\Length(min=2, max=50)
|
||||||
*/
|
*/
|
||||||
private $name;
|
private $name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||||
*/
|
*/
|
||||||
private $value;
|
private $value;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $type;
|
private $type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var bool
|
* @var bool
|
||||||
*/
|
*/
|
||||||
protected $enabled = true;
|
private $enabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Constraint[]
|
* @var Constraint[]
|
||||||
*/
|
*/
|
||||||
protected $constraints = [];
|
private $constraints = [];
|
||||||
|
/**
|
||||||
|
* An array of options for the form element
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $options = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
@@ -237,4 +236,27 @@ class UserPreference
|
|||||||
{
|
{
|
||||||
return $this->constraints;
|
return $this->constraints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an array of options for the FormType.
|
||||||
|
*
|
||||||
|
* @param array $options
|
||||||
|
* @return UserPreference
|
||||||
|
*/
|
||||||
|
public function setOptions(array $options): UserPreference
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array with options for the FormType.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOptions(): array
|
||||||
|
{
|
||||||
|
return $this->options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ final class MenuSubscriber implements EventSubscriberInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($auth->isGranted('view_team')) {
|
||||||
|
$menu->addChild(
|
||||||
|
new MenuItemModel('user_team', 'menu.admin_team', 'admin_team', [], $this->getIcon('team'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($auth->isGranted('plugins')) {
|
if ($auth->isGranted('plugins')) {
|
||||||
$menu->addChild(
|
$menu->addChild(
|
||||||
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $this->getIcon('plugin'))
|
new MenuItemModel('plugins', 'menu.plugin', 'plugins', [], $this->getIcon('plugin'))
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
return $this->formConfig->getUserDefaultTheme();
|
return $this->formConfig->getUserDefaultTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getDefaultCurrency(): ?string
|
||||||
|
{
|
||||||
|
return $this->formConfig->getUserDefaultCurrency();
|
||||||
|
}
|
||||||
|
|
||||||
private function getDefaultLanguage(): string
|
private function getDefaultLanguage(): string
|
||||||
{
|
{
|
||||||
return $this->formConfig->getUserDefaultLanguage();
|
return $this->formConfig->getUserDefaultLanguage();
|
||||||
@@ -88,9 +93,11 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
public function getDefaultPreferences(User $user)
|
public function getDefaultPreferences(User $user)
|
||||||
{
|
{
|
||||||
$enableHourlyRate = false;
|
$enableHourlyRate = false;
|
||||||
|
$hourlyRateOptions = [];
|
||||||
|
|
||||||
if ($this->voter->isGranted('hourly-rate', $user)) {
|
if ($this->voter->isGranted('hourly-rate', $user)) {
|
||||||
$enableHourlyRate = true;
|
$enableHourlyRate = true;
|
||||||
|
$hourlyRateOptions = ['currency' => $this->getDefaultCurrency()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -99,6 +106,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
->setValue(0)
|
->setValue(0)
|
||||||
->setType(MoneyType::class)
|
->setType(MoneyType::class)
|
||||||
->setEnabled($enableHourlyRate)
|
->setEnabled($enableHourlyRate)
|
||||||
|
->setOptions($hourlyRateOptions)
|
||||||
->addConstraint(new Range(['min' => 0])),
|
->addConstraint(new Range(['min' => 0])),
|
||||||
|
|
||||||
(new UserPreference())
|
(new UserPreference())
|
||||||
@@ -165,6 +173,7 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
|||||||
->setType($preference->getType())
|
->setType($preference->getType())
|
||||||
->setConstraints($preference->getConstraints())
|
->setConstraints($preference->getConstraints())
|
||||||
->setEnabled($preference->isEnabled())
|
->setEnabled($preference->isEnabled())
|
||||||
|
->setOptions($preference->getOptions())
|
||||||
;
|
;
|
||||||
} else {
|
} else {
|
||||||
$prefs[$preference->getName()] = $preference;
|
$prefs[$preference->getName()] = $preference;
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ class ActivityEditForm extends AbstractType
|
|||||||
if ($options['customer']) {
|
if ($options['customer']) {
|
||||||
$builder
|
$builder
|
||||||
->add('customer', CustomerType::class, [
|
->add('customer', CustomerType::class, [
|
||||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
$query = new CustomerFormTypeQuery($customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
'data' => $customer ? $customer : null,
|
'data' => $customer ? $customer : null,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
@@ -80,15 +83,18 @@ class ActivityEditForm extends AbstractType
|
|||||||
$builder
|
$builder
|
||||||
->add('project', ProjectType::class, [
|
->add('project', ProjectType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
$query = new ProjectFormTypeQuery($project, $customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||||
$builder->addEventListener(
|
$builder->addEventListener(
|
||||||
FormEvents::PRE_SUBMIT,
|
FormEvents::PRE_SUBMIT,
|
||||||
function (FormEvent $event) use ($project) {
|
function (FormEvent $event) use ($builder, $project) {
|
||||||
$data = $event->getData();
|
$data = $event->getData();
|
||||||
if (!isset($data['customer']) || empty($data['customer'])) {
|
if (!isset($data['customer']) || empty($data['customer'])) {
|
||||||
return;
|
return;
|
||||||
@@ -96,8 +102,11 @@ class ActivityEditForm extends AbstractType
|
|||||||
|
|
||||||
$event->getForm()->add('project', ProjectType::class, [
|
$event->getForm()->add('project', ProjectType::class, [
|
||||||
'group_by' => null,
|
'group_by' => null,
|
||||||
'query_builder' => function (ProjectRepository $repo) use ($data, $project) {
|
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $project) {
|
||||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $data['customer']));
|
$query = new ProjectFormTypeQuery($project, $data['customer']);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/Form/CustomerTeamPermissionForm.php
Normal file
51
src/Form/CustomerTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Form\Type\TeamType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class CustomerTeamPermissionForm extends AbstractType
|
||||||
|
{
|
||||||
|
use EntityFormTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('teams', TeamType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'by_reference' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Customer::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'admin_customer_teams_edit',
|
||||||
|
'attr' => [
|
||||||
|
'data-form-event' => 'kimai.customerTeamUpdate'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Form/Extension/UserExtension.php
Normal file
45
src/Form/Extension/UserExtension.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\Form\Extension;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Security\CurrentUser;
|
||||||
|
use Symfony\Component\Form\AbstractTypeExtension;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
final class UserExtension extends AbstractTypeExtension
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CurrentUser
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
public function __construct(CurrentUser $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getExtendedTypes(): iterable
|
||||||
|
{
|
||||||
|
return [FormType::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param OptionsResolver $resolver
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefined(['user']);
|
||||||
|
// null needs to be allowed, as there is no user for anonymoud forms (like "forgot password" and "registration")
|
||||||
|
$resolver->setAllowedTypes('user', [User::class, 'null']);
|
||||||
|
$resolver->setDefault('user', $this->user->getUser());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,8 +59,11 @@ class ProjectEditForm extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
])
|
])
|
||||||
->add('customer', CustomerType::class, [
|
->add('customer', CustomerType::class, [
|
||||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
$query = new CustomerFormTypeQuery($customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
51
src/Form/ProjectTeamPermissionForm.php
Normal file
51
src/Form/ProjectTeamPermissionForm.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Form\Type\TeamType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class ProjectTeamPermissionForm extends AbstractType
|
||||||
|
{
|
||||||
|
use EntityFormTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('teams', TeamType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'by_reference' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Project::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'admin_project_teams_edit',
|
||||||
|
'attr' => [
|
||||||
|
'data-form-event' => 'kimai.projectTeamUpdate'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/Form/TeamCustomerForm.php
Normal file
51
src/Form/TeamCustomerForm.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Form\Type\CustomerType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TeamCustomerForm extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('customers', CustomerType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'by_reference' => false,
|
||||||
|
'query_builder_for_user' => false,
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Team::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'admin_team_customer',
|
||||||
|
'attr' => [
|
||||||
|
'data-form-event' => 'kimai.teamUpdate'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/Form/TeamEditForm.php
Normal file
61
src/Form/TeamEditForm.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?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\Entity\Team;
|
||||||
|
use App\Form\Type\UserType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TeamEditForm extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('name', TextType::class, [
|
||||||
|
'label' => 'label.name',
|
||||||
|
'attr' => [
|
||||||
|
'autofocus' => 'autofocus'
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->add('teamlead', UserType::class, [
|
||||||
|
'label' => 'label.teamlead',
|
||||||
|
'multiple' => false,
|
||||||
|
'expanded' => false,
|
||||||
|
])
|
||||||
|
->add('users', UserType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
'by_reference' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Team::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'admin_team_edit',
|
||||||
|
'attr' => [
|
||||||
|
'data-form-event' => 'kimai.teamUpdate'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Form/TeamProjectForm.php
Normal file
52
src/Form/TeamProjectForm.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?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\Entity\Team;
|
||||||
|
use App\Form\Type\ProjectType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TeamProjectForm extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('projects', ProjectType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => false,
|
||||||
|
'by_reference' => false,
|
||||||
|
'attr' => ['size' => '20'],
|
||||||
|
'query_builder_for_user' => false,
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => Team::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'admin_team_project',
|
||||||
|
'attr' => [
|
||||||
|
'data-form-event' => 'kimai.teamUpdate'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,8 +163,11 @@ class TimesheetEditForm extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('customer', CustomerType::class, [
|
->add('customer', CustomerType::class, [
|
||||||
'query_builder' => function (CustomerRepository $repo) use ($customer) {
|
'query_builder' => function (CustomerRepository $repo) use ($builder, $customer) {
|
||||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery($customer));
|
$query = new CustomerFormTypeQuery($customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
'data' => $customer ? $customer : '',
|
'data' => $customer ? $customer : '',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
@@ -189,8 +192,11 @@ class TimesheetEditForm extends AbstractType
|
|||||||
array_merge($projectOptions, [
|
array_merge($projectOptions, [
|
||||||
'placeholder' => '',
|
'placeholder' => '',
|
||||||
'activity_enabled' => true,
|
'activity_enabled' => true,
|
||||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer) {
|
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
|
||||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
$query = new ProjectFormTypeQuery($project, $customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@@ -198,7 +204,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
// replaces the project select after submission, to make sure only projects for the selected customer are displayed
|
||||||
$builder->addEventListener(
|
$builder->addEventListener(
|
||||||
FormEvents::PRE_SUBMIT,
|
FormEvents::PRE_SUBMIT,
|
||||||
function (FormEvent $event) use ($project, $customer, $isNew) {
|
function (FormEvent $event) use ($builder, $project, $customer, $isNew) {
|
||||||
$data = $event->getData();
|
$data = $event->getData();
|
||||||
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
|
||||||
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
|
||||||
@@ -207,7 +213,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
'placeholder' => '',
|
'placeholder' => '',
|
||||||
'activity_enabled' => true,
|
'activity_enabled' => true,
|
||||||
'group_by' => null,
|
'group_by' => null,
|
||||||
'query_builder' => function (ProjectRepository $repo) use ($project, $customer, $isNew) {
|
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
|
||||||
// is there a better wa to prevent starting a record with a hidden project ?
|
// is there a better wa to prevent starting a record with a hidden project ?
|
||||||
if ($isNew && !is_object($project)) {
|
if ($isNew && !is_object($project)) {
|
||||||
/** @var Project $project */
|
/** @var Project $project */
|
||||||
@@ -221,8 +227,10 @@ class TimesheetEditForm extends AbstractType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$query = new ProjectFormTypeQuery($project, $customer);
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery($project, $customer));
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,13 +66,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
|||||||
|
|
||||||
$builder->addEventListener(
|
$builder->addEventListener(
|
||||||
FormEvents::PRE_SUBMIT,
|
FormEvents::PRE_SUBMIT,
|
||||||
function (FormEvent $event) {
|
function (FormEvent $event) use ($builder) {
|
||||||
$data = $event->getData();
|
$data = $event->getData();
|
||||||
$event->getForm()->add('customer', CustomerType::class, [
|
$event->getForm()->add('customer', CustomerType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'project_enabled' => true,
|
'project_enabled' => true,
|
||||||
'query_builder' => function (CustomerRepository $repo) use ($data) {
|
'query_builder' => function (CustomerRepository $repo) use ($builder, $data) {
|
||||||
$query = new CustomerFormTypeQuery();
|
$query = new CustomerFormTypeQuery();
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||||
$query->setCustomer($data['customer']);
|
$query->setCustomer($data['customer']);
|
||||||
}
|
}
|
||||||
@@ -138,13 +139,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
|||||||
|
|
||||||
$builder->addEventListener(
|
$builder->addEventListener(
|
||||||
FormEvents::PRE_SUBMIT,
|
FormEvents::PRE_SUBMIT,
|
||||||
function (FormEvent $event) {
|
function (FormEvent $event) use ($builder) {
|
||||||
$data = $event->getData();
|
$data = $event->getData();
|
||||||
$event->getForm()->add('project', ProjectType::class, [
|
$event->getForm()->add('project', ProjectType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'activity_enabled' => true,
|
'activity_enabled' => true,
|
||||||
'query_builder' => function (ProjectRepository $repo) use ($data) {
|
'query_builder' => function (ProjectRepository $repo) use ($builder, $data) {
|
||||||
$query = new ProjectFormTypeQuery();
|
$query = new ProjectFormTypeQuery();
|
||||||
|
$query->setUser($builder->getOption('user'));
|
||||||
|
|
||||||
if (isset($data['customer']) && !empty($data['customer'])) {
|
if (isset($data['customer']) && !empty($data['customer'])) {
|
||||||
$query->setCustomer($data['customer']);
|
$query->setCustomer($data['customer']);
|
||||||
|
|||||||
37
src/Form/Toolbar/TeamToolbarForm.php
Normal file
37
src/Form/Toolbar/TeamToolbarForm.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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\Toolbar;
|
||||||
|
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TeamToolbarForm extends AbstractToolbarForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$this->addPageSizeChoice($builder);
|
||||||
|
$this->addHiddenPagination($builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => TeamQuery::class,
|
||||||
|
'csrf_protection' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,13 +37,22 @@ class CustomerType extends AbstractType
|
|||||||
'label' => 'label.customer',
|
'label' => 'label.customer',
|
||||||
'class' => Customer::class,
|
'class' => Customer::class,
|
||||||
'choice_label' => 'name',
|
'choice_label' => 'name',
|
||||||
'query_builder' => function (CustomerRepository $repo) {
|
'query_builder_for_user' => true,
|
||||||
return $repo->getQueryBuilderForFormType(new CustomerFormTypeQuery());
|
|
||||||
},
|
|
||||||
'project_enabled' => false,
|
'project_enabled' => false,
|
||||||
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
|
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
|
return function (CustomerRepository $repo) use ($options) {
|
||||||
|
$query = new CustomerFormTypeQuery();
|
||||||
|
if (true === $options['query_builder_for_user']) {
|
||||||
|
$query->setUser($options['user']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
$resolver->setDefault('api_data', function (Options $options) {
|
$resolver->setDefault('api_data', function (Options $options) {
|
||||||
if (true === $options['project_enabled']) {
|
if (true === $options['project_enabled']) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -62,13 +62,22 @@ class ProjectType extends AbstractType
|
|||||||
'group_by' => function (Project $project, $key, $index) {
|
'group_by' => function (Project $project, $key, $index) {
|
||||||
return $project->getCustomer()->getName();
|
return $project->getCustomer()->getName();
|
||||||
},
|
},
|
||||||
'query_builder' => function (ProjectRepository $repo) {
|
'query_builder_for_user' => true,
|
||||||
return $repo->getQueryBuilderForFormType(new ProjectFormTypeQuery());
|
|
||||||
},
|
|
||||||
'activity_enabled' => false,
|
'activity_enabled' => false,
|
||||||
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
'activity_visibility' => ActivityQuery::SHOW_VISIBLE,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
|
return function (ProjectRepository $repo) use ($options) {
|
||||||
|
$query = new ProjectFormTypeQuery();
|
||||||
|
if (true === $options['query_builder_for_user']) {
|
||||||
|
$query->setUser($options['user']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
$resolver->setDefault('api_data', function (Options $options) {
|
$resolver->setDefault('api_data', function (Options $options) {
|
||||||
if (true === $options['activity_enabled']) {
|
if (true === $options['activity_enabled']) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
44
src/Form/Type/TeamType.php
Normal file
44
src/Form/Type/TeamType.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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\Entity\Team;
|
||||||
|
use App\Repository\TeamRepository;
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TeamType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'class' => Team::class,
|
||||||
|
'label' => 'label.team',
|
||||||
|
'query_builder' => function (TeamRepository $repo) {
|
||||||
|
return $repo->createQueryBuilder('t')->orderBy('t.name', 'ASC');
|
||||||
|
},
|
||||||
|
'choice_label' => function (Team $team) {
|
||||||
|
return $team->getName();
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return EntityType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,23 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
use Symfony\Component\Form\FormEvent;
|
use Symfony\Component\Form\FormEvent;
|
||||||
use Symfony\Component\Form\FormEvents;
|
use Symfony\Component\Form\FormEvents;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom form field type to edit a user preference.
|
* Custom form field type to edit a user preference.
|
||||||
*/
|
*/
|
||||||
class UserPreferenceType extends AbstractType
|
class UserPreferenceType extends AbstractType
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translate;
|
||||||
|
|
||||||
|
public function __construct(TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->translate = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param FormBuilderInterface $builder
|
* @param FormBuilderInterface $builder
|
||||||
* @param array $options
|
* @param array $options
|
||||||
@@ -54,12 +65,22 @@ class UserPreferenceType extends AbstractType
|
|||||||
$type = HiddenType::class;
|
$type = HiddenType::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
$event->getForm()->add('value', $type, [
|
$transId = 'label.' . $preference->getName();
|
||||||
'label' => 'label.' . $preference->getName(),
|
if ($this->translate->trans($transId) === $transId) {
|
||||||
'constraints' => $preference->getConstraints(),
|
$transId = $preference->getName();
|
||||||
'required' => $required,
|
}
|
||||||
'disabled' => !$preference->isEnabled(),
|
|
||||||
]);
|
$options = array_merge(
|
||||||
|
[
|
||||||
|
'label' => $transId,
|
||||||
|
'constraints' => $preference->getConstraints(),
|
||||||
|
'required' => $required,
|
||||||
|
'disabled' => !$preference->isEnabled(),
|
||||||
|
],
|
||||||
|
$preference->getOptions()
|
||||||
|
);
|
||||||
|
|
||||||
|
$event->getForm()->add('value', $type, $options);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
$builder->add('name', HiddenType::class);
|
$builder->add('name', HiddenType::class);
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
namespace App\Form\Type;
|
namespace App\Form\Type;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Repository\Query\UserFormTypeQuery;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +38,15 @@ class UserType extends AbstractType
|
|||||||
return $user->getUsername();
|
return $user->getUsername();
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
|
return function (UserRepository $repo) use ($options) {
|
||||||
|
$query = new UserFormTypeQuery();
|
||||||
|
$query->setUser($options['user']);
|
||||||
|
|
||||||
|
return $repo->getQueryBuilderForFormType($query);
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
49
src/Form/UserTeamsType.php
Normal file
49
src/Form/UserTeamsType.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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\Entity\User;
|
||||||
|
use App\Form\Type\TeamType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used to assign a User to teams.
|
||||||
|
*/
|
||||||
|
class UserTeamsType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('teams', TeamType::class, [
|
||||||
|
'label' => 'label.team',
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => true,
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => User::class,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'csrf_field_name' => '_token',
|
||||||
|
'csrf_token_id' => 'edit_user_teams',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,13 +39,6 @@ class Version20190510205245 extends AbstractMigration
|
|||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
$tags = $schema->getTable('kimai2_tags');
|
|
||||||
$tags->dropIndex('UNIQ_27CAF54C5E237E06');
|
|
||||||
|
|
||||||
$timesheetTags = $schema->getTable('kimai2_timesheet_tags');
|
|
||||||
$timesheetTags->dropIndex('IDX_E3284EFEABDD46BE');
|
|
||||||
$timesheetTags->dropIndex('IDX_E3284EFEBAD26311');
|
|
||||||
|
|
||||||
$schema->dropTable('kimai2_timesheet_tags');
|
$schema->dropTable('kimai2_timesheet_tags');
|
||||||
$schema->dropTable('kimai2_tags');
|
$schema->dropTable('kimai2_tags');
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/Migrations/Version20190729162655.php
Normal file
44
src/Migrations/Version20190729162655.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates user team tables.
|
||||||
|
*
|
||||||
|
* @version 1.2
|
||||||
|
*/
|
||||||
|
final class Version20190729162655 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Fixes foreign keys on tag table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$timesheetTags = $schema->getTable('kimai2_timesheet_tags');
|
||||||
|
|
||||||
|
if (!$timesheetTags->hasForeignKey('FK_732EECA9ABDD46BE')) {
|
||||||
|
$timesheetTags->addForeignKeyConstraint('kimai2_timesheet', ['timesheet_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_732EECA9ABDD46BE');
|
||||||
|
}
|
||||||
|
if (!$timesheetTags->hasForeignKey('FK_732EECA9BAD26311')) {
|
||||||
|
$timesheetTags->addForeignKeyConstraint('kimai2_tags', ['tag_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_732EECA9BAD26311');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/Migrations/Version20190730123324.php
Normal file
68
src/Migrations/Version20190730123324.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes foreign keys on tag table.
|
||||||
|
*
|
||||||
|
* @version 1.2
|
||||||
|
*/
|
||||||
|
final class Version20190730123324 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Creates user team and permission tables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$teams = $schema->createTable('kimai2_teams');
|
||||||
|
$teams->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$teams->addColumn('name', 'string', ['notnull' => true, 'length' => 100]);
|
||||||
|
$teams->addColumn('teamlead_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$teams->setPrimaryKey(['id']);
|
||||||
|
$teams->addUniqueIndex(['name'], 'UNIQ_3BEDDC7F5E237E06');
|
||||||
|
$teams->addForeignKeyConstraint('kimai2_users', ['teamlead_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_3BEDDC7F8F7DE5D7');
|
||||||
|
|
||||||
|
$userTeams = $schema->createTable('kimai2_users_teams');
|
||||||
|
$userTeams->addColumn('user_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$userTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$userTeams->addForeignKeyConstraint('kimai2_users', ['user_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8A76ED395');
|
||||||
|
$userTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_B5E92CF8296CD8AE');
|
||||||
|
$userTeams->setPrimaryKey(['user_id', 'team_id']);
|
||||||
|
|
||||||
|
$customerTeams = $schema->createTable('kimai2_customers_teams');
|
||||||
|
$customerTeams->addColumn('customer_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$customerTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$customerTeams->addForeignKeyConstraint('kimai2_customers', ['customer_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD83889395C3F3');
|
||||||
|
$customerTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_50BD8388296CD8AE');
|
||||||
|
$customerTeams->setPrimaryKey(['customer_id', 'team_id']);
|
||||||
|
|
||||||
|
$projectTeams = $schema->createTable('kimai2_projects_teams');
|
||||||
|
$projectTeams->addColumn('project_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$projectTeams->addColumn('team_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||||
|
$projectTeams->addForeignKeyConstraint('kimai2_projects', ['project_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431166D1F9C');
|
||||||
|
$projectTeams->addForeignKeyConstraint('kimai2_teams', ['team_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_9345D431296CD8AE');
|
||||||
|
$projectTeams->setPrimaryKey(['project_id', 'team_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$schema->dropTable('kimai2_projects_teams');
|
||||||
|
$schema->dropTable('kimai2_customers_teams');
|
||||||
|
$schema->dropTable('kimai2_users_teams');
|
||||||
|
$schema->dropTable('kimai2_teams');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ namespace App\Repository;
|
|||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Model\ActivityStatistic;
|
use App\Model\ActivityStatistic;
|
||||||
use App\Repository\Loader\ActivityLoader;
|
use App\Repository\Loader\ActivityLoader;
|
||||||
use App\Repository\Paginator\LoaderPaginator;
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
@@ -81,6 +82,47 @@ class ActivityRepository extends EntityRepository
|
|||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||||
|
{
|
||||||
|
// make sure that all queries without a user see all projects
|
||||||
|
if (null === $user && empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that admins see all activities
|
||||||
|
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $user) {
|
||||||
|
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->leftJoin('p.teams', 'teams')
|
||||||
|
->leftJoin('c.teams', 'c_teams');
|
||||||
|
|
||||||
|
if (empty($teams)) {
|
||||||
|
$qb->andWhere($qb->expr()->isNull('c_teams'));
|
||||||
|
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orProject = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($orProject);
|
||||||
|
|
||||||
|
$orCustomer = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('c_teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($orCustomer);
|
||||||
|
|
||||||
|
$qb->setParameter('teams', $teams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated since 1.1
|
* @deprecated since 1.1
|
||||||
*/
|
*/
|
||||||
@@ -181,16 +223,11 @@ class ActivityRepository extends EntityRepository
|
|||||||
$qb
|
$qb
|
||||||
->select('a')
|
->select('a')
|
||||||
->from(Activity::class, 'a')
|
->from(Activity::class, 'a')
|
||||||
|
->leftJoin('a.project', 'p')
|
||||||
|
->leftJoin('p.customer', 'c')
|
||||||
->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder())
|
->addOrderBy('a.' . $query->getOrderBy(), $query->getOrder())
|
||||||
;
|
;
|
||||||
|
|
||||||
if (!$query->isGlobalsOnly()) {
|
|
||||||
$qb
|
|
||||||
->leftJoin('a.project', 'p')
|
|
||||||
->leftJoin('p.customer', 'c')
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
$where = $qb->expr()->andX();
|
$where = $qb->expr()->andX();
|
||||||
|
|
||||||
if (in_array($query->getVisibility(), [ActivityQuery::SHOW_VISIBLE, ActivityQuery::SHOW_HIDDEN])) {
|
if (in_array($query->getVisibility(), [ActivityQuery::SHOW_VISIBLE, ActivityQuery::SHOW_HIDDEN])) {
|
||||||
@@ -239,6 +276,8 @@ class ActivityRepository extends EntityRepository
|
|||||||
$qb->andWhere($where);
|
$qb->andWhere($where);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Activity;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Model\CustomerStatistic;
|
use App\Model\CustomerStatistic;
|
||||||
use App\Repository\Loader\CustomerLoader;
|
use App\Repository\Loader\CustomerLoader;
|
||||||
use App\Repository\Paginator\LoaderPaginator;
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
@@ -107,8 +108,41 @@ class CustomerRepository extends EntityRepository
|
|||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||||
|
{
|
||||||
|
// make sure that all queries without a user see all customers
|
||||||
|
if (null === $user && empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that admins see all customers
|
||||||
|
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $user) {
|
||||||
|
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->leftJoin('c.teams', 'teams');
|
||||||
|
|
||||||
|
if (empty($teams)) {
|
||||||
|
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$or = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($or);
|
||||||
|
|
||||||
|
$qb->setParameter('teams', $teams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated since 1.1
|
* @deprecated since 1.1 - don't use this method, it ignores team permission checks
|
||||||
*/
|
*/
|
||||||
public function builderForEntityType($customer)
|
public function builderForEntityType($customer)
|
||||||
{
|
{
|
||||||
@@ -145,6 +179,8 @@ class CustomerRepository extends EntityRepository
|
|||||||
$qb->setParameter('ignored', $query->getCustomerToIgnore());
|
$qb->setParameter('ignored', $query->getCustomerToIgnore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +188,7 @@ class CustomerRepository extends EntityRepository
|
|||||||
{
|
{
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
$qb->select('c', 'meta')
|
$qb->select('c')
|
||||||
->from(Customer::class, 'c')
|
->from(Customer::class, 'c')
|
||||||
->leftJoin('c.meta', 'meta')
|
->leftJoin('c.meta', 'meta')
|
||||||
->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
|
->orderBy('c.' . $query->getOrderBy(), $query->getOrder());
|
||||||
@@ -165,6 +201,8 @@ class CustomerRepository extends EntityRepository
|
|||||||
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
|
$qb->setParameter('visible', false, \PDO::PARAM_BOOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,27 @@ final class ActivityIdLoader implements LoaderInterface
|
|||||||
->andWhere($qb->expr()->in('p.id', $projectIds))
|
->andWhere($qb->expr()->in('p.id', $projectIds))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL a.{id}', 'PARTIAL project.{id}', 'teams', 'teamlead')
|
||||||
|
->from(Activity::class, 'a')
|
||||||
|
->leftJoin('a.project', 'project')
|
||||||
|
->leftJoin('project.teams', 'teams')
|
||||||
|
->leftJoin('teams.teamlead', 'teamlead')
|
||||||
|
->andWhere($qb->expr()->in('a.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL a.{id}', 'PARTIAL project.{id}', 'PARTIAL customer.{id}', 'teams', 'teamlead')
|
||||||
|
->from(Activity::class, 'a')
|
||||||
|
->leftJoin('a.project', 'project')
|
||||||
|
->leftJoin('project.customer', 'customer')
|
||||||
|
->leftJoin('customer.teams', 'teams')
|
||||||
|
->leftJoin('teams.teamlead', 'teamlead')
|
||||||
|
->andWhere($qb->expr()->in('a.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,14 @@ final class CustomerIdLoader implements LoaderInterface
|
|||||||
->andWhere($qb->expr()->in('c.id', $ids))
|
->andWhere($qb->expr()->in('c.id', $ids))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL c.{id}', 'teams', 'teamlead')
|
||||||
|
->from(Customer::class, 'c')
|
||||||
|
->leftJoin('c.teams', 'teams')
|
||||||
|
->leftJoin('teams.teamlead', 'teamlead')
|
||||||
|
->andWhere($qb->expr()->in('c.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,5 +50,24 @@ final class ProjectIdLoader implements LoaderInterface
|
|||||||
->andWhere($qb->expr()->in('p.id', $ids))
|
->andWhere($qb->expr()->in('p.id', $ids))
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL p.{id}', 'teams', 'teamlead')
|
||||||
|
->from(Project::class, 'p')
|
||||||
|
->leftJoin('p.teams', 'teams')
|
||||||
|
->leftJoin('teams.teamlead', 'teamlead')
|
||||||
|
->andWhere($qb->expr()->in('p.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL p.{id}', 'PARTIAL customer.{id}', 'teams', 'teamlead')
|
||||||
|
->from(Project::class, 'p')
|
||||||
|
->leftJoin('p.customer', 'customer')
|
||||||
|
->leftJoin('customer.teams', 'teams')
|
||||||
|
->leftJoin('teams.teamlead', 'teamlead')
|
||||||
|
->andWhere($qb->expr()->in('p.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/Repository/Loader/TeamIdLoader.php
Normal file
46
src/Repository/Loader/TeamIdLoader.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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\Loader;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
final class TeamIdLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EntityManagerInterface
|
||||||
|
*/
|
||||||
|
private $entityManager;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int[] $ids
|
||||||
|
*/
|
||||||
|
public function loadResults(array $ids): void
|
||||||
|
{
|
||||||
|
if (empty($ids)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$em = $this->entityManager;
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL t.{id}', 'users')
|
||||||
|
->from(Team::class, 't')
|
||||||
|
->leftJoin('t.users', 'users')
|
||||||
|
->andWhere($qb->expr()->in('t.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Repository/Loader/TeamLoader.php
Normal file
38
src/Repository/Loader/TeamLoader.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?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\Loader;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
final class TeamLoader implements LoaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TeamIdLoader
|
||||||
|
*/
|
||||||
|
private $loader;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->loader = new TeamIdLoader($entityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Team[] $teams
|
||||||
|
*/
|
||||||
|
public function loadResults(array $teams): void
|
||||||
|
{
|
||||||
|
$ids = array_map(function (Team $team) {
|
||||||
|
return $team->getId();
|
||||||
|
}, $teams);
|
||||||
|
|
||||||
|
$this->loader->loadResults($ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace App\Repository;
|
|||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Model\ProjectStatistic;
|
use App\Model\ProjectStatistic;
|
||||||
use App\Repository\Loader\ProjectLoader;
|
use App\Repository\Loader\ProjectLoader;
|
||||||
use App\Repository\Paginator\LoaderPaginator;
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
@@ -91,8 +92,49 @@ class ProjectRepository extends EntityRepository
|
|||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||||
|
{
|
||||||
|
// make sure that all queries without a user see all projects
|
||||||
|
if (null === $user && empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that admins see all projects
|
||||||
|
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $user) {
|
||||||
|
$teams = array_merge($teams, $user->getTeams()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb->leftJoin('p.teams', 'teams')
|
||||||
|
->leftJoin('c.teams', 'c_teams');
|
||||||
|
|
||||||
|
if (empty($teams)) {
|
||||||
|
$qb->andWhere($qb->expr()->isNull('c_teams'));
|
||||||
|
$qb->andWhere($qb->expr()->isNull('teams'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orProject = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'p.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($orProject);
|
||||||
|
|
||||||
|
$orCustomer = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('c_teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($orCustomer);
|
||||||
|
|
||||||
|
$qb->setParameter('teams', $teams);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated since 1.1
|
* @deprecated since 1.1 - don't use this method, it ignores team permission checks
|
||||||
*/
|
*/
|
||||||
public function builderForEntityType($project, $customer)
|
public function builderForEntityType($project, $customer)
|
||||||
{
|
{
|
||||||
@@ -114,7 +156,7 @@ class ProjectRepository extends EntityRepository
|
|||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
$qb
|
$qb
|
||||||
->select('p', 'c')
|
->select('p')
|
||||||
->from(Project::class, 'p')
|
->from(Project::class, 'p')
|
||||||
->leftJoin('p.customer', 'c')
|
->leftJoin('p.customer', 'c')
|
||||||
->addOrderBy('c.name', 'ASC')
|
->addOrderBy('c.name', 'ASC')
|
||||||
@@ -140,6 +182,8 @@ class ProjectRepository extends EntityRepository
|
|||||||
$qb->setParameter('ignored', $query->getProjectToIgnore());
|
$qb->setParameter('ignored', $query->getProjectToIgnore());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +194,11 @@ class ProjectRepository extends EntityRepository
|
|||||||
$qb
|
$qb
|
||||||
->select('p')
|
->select('p')
|
||||||
->from(Project::class, 'p')
|
->from(Project::class, 'p')
|
||||||
|
->leftJoin('p.customer', 'c')
|
||||||
;
|
;
|
||||||
|
|
||||||
if (in_array($query->getVisibility(), [ProjectQuery::SHOW_VISIBLE, ProjectQuery::SHOW_HIDDEN])) {
|
if (in_array($query->getVisibility(), [ProjectQuery::SHOW_VISIBLE, ProjectQuery::SHOW_HIDDEN])) {
|
||||||
$qb
|
$qb
|
||||||
->leftJoin('p.customer', 'c')
|
|
||||||
->andWhere($qb->expr()->eq('p.visible', ':visible'))
|
->andWhere($qb->expr()->eq('p.visible', ':visible'))
|
||||||
->andWhere($qb->expr()->eq('c.visible', ':customer_visible'))
|
->andWhere($qb->expr()->eq('c.visible', ':customer_visible'))
|
||||||
;
|
;
|
||||||
@@ -173,6 +217,8 @@ class ProjectRepository extends EntityRepository
|
|||||||
->setParameter('customer', $query->getCustomer());
|
->setParameter('customer', $query->getCustomer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||||
|
|
||||||
$qb->orderBy('p.' . $query->getOrderBy(), $query->getOrder());
|
$qb->orderBy('p.' . $query->getOrderBy(), $query->getOrder());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
namespace App\Repository\Query;
|
namespace App\Repository\Query;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for advanced Repository queries.
|
* Base class for advanced Repository queries.
|
||||||
*/
|
*/
|
||||||
@@ -44,6 +47,45 @@ class BaseQuery
|
|||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $resultType = self::RESULT_TYPE_PAGER;
|
private $resultType = self::RESULT_TYPE_PAGER;
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
/**
|
||||||
|
* @var Team[]
|
||||||
|
*/
|
||||||
|
private $teams = [];
|
||||||
|
|
||||||
|
public function addTeam(Team $team): self
|
||||||
|
{
|
||||||
|
$this->teams[$team->getId()] = $team;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Team[]
|
||||||
|
*/
|
||||||
|
public function getTeams(): array
|
||||||
|
{
|
||||||
|
return array_values($this->teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setCurrentUser(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
namespace App\Repository\Query;
|
namespace App\Repository\Query;
|
||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be used for advanced queries with the: CustomerRepository
|
* Can be used for advanced queries with the: CustomerRepository
|
||||||
@@ -24,6 +26,14 @@ final class CustomerFormTypeQuery
|
|||||||
* @var Customer|null
|
* @var Customer|null
|
||||||
*/
|
*/
|
||||||
private $customerToIgnore;
|
private $customerToIgnore;
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
/**
|
||||||
|
* @var array<Team>
|
||||||
|
*/
|
||||||
|
private $teams = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Customer|int|null $customer
|
* @param Customer|int|null $customer
|
||||||
@@ -33,6 +43,33 @@ final class CustomerFormTypeQuery
|
|||||||
$this->customer = $customer;
|
$this->customer = $customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team): CustomerFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->teams[$team->getId()] = $team;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Team[]
|
||||||
|
*/
|
||||||
|
public function getTeams(): array
|
||||||
|
{
|
||||||
|
return array_values($this->teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(User $user): CustomerFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Customer|int|null
|
* @return Customer|int|null
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace App\Repository\Query;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
final class ProjectFormTypeQuery
|
final class ProjectFormTypeQuery
|
||||||
{
|
{
|
||||||
@@ -26,6 +28,14 @@ final class ProjectFormTypeQuery
|
|||||||
* @var Project|null
|
* @var Project|null
|
||||||
*/
|
*/
|
||||||
private $projectToIgnore;
|
private $projectToIgnore;
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
/**
|
||||||
|
* @var array<Team>
|
||||||
|
*/
|
||||||
|
private $teams = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Project|int|null $project
|
* @param Project|int|null $project
|
||||||
@@ -37,6 +47,33 @@ final class ProjectFormTypeQuery
|
|||||||
$this->customer = $customer;
|
$this->customer = $customer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team): ProjectFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->teams[$team->getId()] = $team;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Team[]
|
||||||
|
*/
|
||||||
|
public function getTeams(): array
|
||||||
|
{
|
||||||
|
return array_values($this->teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(User $user): ProjectFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Customer|int|null
|
* @return Customer|int|null
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,4 +11,8 @@ namespace App\Repository\Query;
|
|||||||
|
|
||||||
class TagQuery extends BaseQuery
|
class TagQuery extends BaseQuery
|
||||||
{
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setOrderBy('name');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/Repository/Query/TeamQuery.php
Normal file
18
src/Repository/Query/TeamQuery.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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\Query;
|
||||||
|
|
||||||
|
class TeamQuery extends BaseQuery
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setOrderBy('name');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
/**
|
/**
|
||||||
* @var User|null
|
* @var User|null
|
||||||
*/
|
*/
|
||||||
protected $user;
|
protected $timesheetUser;
|
||||||
/**
|
/**
|
||||||
* @var Activity|null
|
* @var Activity|null
|
||||||
*/
|
*/
|
||||||
@@ -63,7 +63,7 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
*/
|
*/
|
||||||
public function getUser()
|
public function getUser()
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->timesheetUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +72,7 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
*/
|
*/
|
||||||
public function setUser($user = null)
|
public function setUser($user = null)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->timesheetUser = $user;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->user !== null) {
|
if ($this->timesheetUser !== null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
src/Repository/Query/UserFormTypeQuery.php
Normal file
55
src/Repository/Query/UserFormTypeQuery.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?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\Query;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used for pre-filling form types with the: UserRepository
|
||||||
|
*/
|
||||||
|
final class UserFormTypeQuery
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
/**
|
||||||
|
* @var array<Team>
|
||||||
|
*/
|
||||||
|
private $teams = [];
|
||||||
|
|
||||||
|
public function addTeam(Team $team): UserFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->teams[$team->getId()] = $team;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Team[]
|
||||||
|
*/
|
||||||
|
public function getTeams(): array
|
||||||
|
{
|
||||||
|
return array_values($this->teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): ?User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(User $user): UserFormTypeQuery
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,26 @@
|
|||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Tag;
|
||||||
use App\Repository\Query\TagQuery;
|
use App\Repository\Query\TagQuery;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
|
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||||
|
use Pagerfanta\Pagerfanta;
|
||||||
|
|
||||||
class TagRepository extends EntityRepository
|
class TagRepository extends EntityRepository
|
||||||
{
|
{
|
||||||
use RepositoryTrait;
|
/**
|
||||||
|
* @param Tag $tag
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function deleteTag(Tag $tag)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->remove($tag);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find ids of the given tagNames separated by comma
|
* Find ids of the given tagNames separated by comma
|
||||||
@@ -68,7 +82,7 @@ class TagRepository extends EntityRepository
|
|||||||
* - amount
|
* - amount
|
||||||
*
|
*
|
||||||
* @param TagQuery $query
|
* @param TagQuery $query
|
||||||
* @return array|\Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
* @return Pagerfanta
|
||||||
*/
|
*/
|
||||||
public function getTagCount(TagQuery $query)
|
public function getTagCount(TagQuery $query)
|
||||||
{
|
{
|
||||||
@@ -79,9 +93,12 @@ class TagRepository extends EntityRepository
|
|||||||
->leftJoin('tag.timesheets', 'timesheets')
|
->leftJoin('tag.timesheets', 'timesheets')
|
||||||
->addGroupBy('tag.id')
|
->addGroupBy('tag.id')
|
||||||
->addGroupBy('tag.name')
|
->addGroupBy('tag.name')
|
||||||
->orderBy('tag.name')
|
->orderBy('tag.' . $query->getOrderBy(), $query->getOrder());
|
||||||
;
|
|
||||||
|
|
||||||
return $this->getBaseQueryResult($qb, $query);
|
$paginator = new Pagerfanta(new DoctrineORMAdapter($qb->getQuery(), false));
|
||||||
|
$paginator->setMaxPerPage($query->getPageSize());
|
||||||
|
$paginator->setCurrentPage($query->getPage());
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/Repository/TeamRepository.php
Normal file
99
src/Repository/TeamRepository.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\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Repository\Loader\TeamLoader;
|
||||||
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
|
use App\Repository\Paginator\PaginatorInterface;
|
||||||
|
use App\Repository\Query\TeamQuery;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Pagerfanta\Pagerfanta;
|
||||||
|
|
||||||
|
class TeamRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Team $team
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function saveTeam(Team $team)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->persist($team);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Team $team
|
||||||
|
* @throws ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function deleteTeam(Team $team)
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->remove($team);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPagerfantaForQuery(TeamQuery $query): Pagerfanta
|
||||||
|
{
|
||||||
|
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||||
|
$paginator->setMaxPerPage($query->getPageSize());
|
||||||
|
$paginator->setCurrentPage($query->getPage());
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPaginatorForQuery(TeamQuery $query): PaginatorInterface
|
||||||
|
{
|
||||||
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
$qb
|
||||||
|
->resetDQLPart('select')
|
||||||
|
->resetDQLPart('orderBy')
|
||||||
|
->select($qb->expr()->countDistinct('t.id'))
|
||||||
|
;
|
||||||
|
$counter = (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
|
|
||||||
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
|
|
||||||
|
return new LoaderPaginator(new TeamLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TeamQuery $query
|
||||||
|
* @return Timesheet[]
|
||||||
|
*/
|
||||||
|
public function getTeamsForQuery(TeamQuery $query): iterable
|
||||||
|
{
|
||||||
|
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||||
|
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||||
|
$paginator = $this->getPaginatorForQuery($query);
|
||||||
|
|
||||||
|
return $paginator->getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getQueryBuilderForQuery(TeamQuery $query): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->select('t')
|
||||||
|
->from(Team::class, 't')
|
||||||
|
;
|
||||||
|
|
||||||
|
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -456,6 +456,36 @@ class TimesheetRepository extends EntityRepository
|
|||||||
return $counter;
|
return $counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||||
|
{
|
||||||
|
// make sure that all queries without a user see all projects
|
||||||
|
if (null === $user && empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that admins see all timesheet records
|
||||||
|
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->leftJoin('p.customer', 'c')
|
||||||
|
->leftJoin('p.teams', 'teams')
|
||||||
|
->leftJoin('c.teams', 'c_teams');
|
||||||
|
|
||||||
|
$orTeam = $qb->expr()->orX(
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'p.teams'),
|
||||||
|
$qb->expr()->isMemberOf(':teams', 'c.teams')
|
||||||
|
);
|
||||||
|
$qb->andWhere($orTeam);
|
||||||
|
|
||||||
|
$qb->setParameter('teams', $teams);
|
||||||
|
}
|
||||||
|
|
||||||
public function getPagerfantaForQuery(TimesheetQuery $query): Pagerfanta
|
public function getPagerfantaForQuery(TimesheetQuery $query): Pagerfanta
|
||||||
{
|
{
|
||||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||||
@@ -500,11 +530,43 @@ class TimesheetRepository extends EntityRepository
|
|||||||
$qb
|
$qb
|
||||||
->select('t')
|
->select('t')
|
||||||
->from(Timesheet::class, 't')
|
->from(Timesheet::class, 't')
|
||||||
|
->leftJoin('t.project', 'p')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
$user = [];
|
||||||
if (null !== $query->getUser()) {
|
if (null !== $query->getUser()) {
|
||||||
$qb->andWhere('t.user = :user')
|
$user[] = $query->getUser();
|
||||||
->setParameter('user', $query->getUser());
|
}
|
||||||
|
|
||||||
|
if (null === $query->getUser() && null !== $query->getCurrentUser()) {
|
||||||
|
$currentUser = $query->getCurrentUser();
|
||||||
|
|
||||||
|
if (!$currentUser->isSuperAdmin() && !$currentUser->isAdmin()) {
|
||||||
|
foreach ($currentUser->getTeams() as $team) {
|
||||||
|
if ($currentUser->isTeamleadOf($team)) {
|
||||||
|
$query->addTeam($team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($query->getTeams())) {
|
||||||
|
foreach ($query->getTeams() as $team) {
|
||||||
|
$user = array_merge($user, $team->getUsers()->toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = array_map(function ($user) {
|
||||||
|
if ($user instanceof User) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}, $user);
|
||||||
|
$user = array_unique($user);
|
||||||
|
|
||||||
|
if (!empty($user)) {
|
||||||
|
$qb->andWhere($qb->expr()->in('t.user', $user));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $query->getBegin()) {
|
if (null !== $query->getBegin()) {
|
||||||
@@ -539,7 +601,6 @@ class TimesheetRepository extends EntityRepository
|
|||||||
$qb->andWhere('t.project = :project')
|
$qb->andWhere('t.project = :project')
|
||||||
->setParameter('project', $query->getProject());
|
->setParameter('project', $query->getProject());
|
||||||
} elseif (null !== $query->getCustomer()) {
|
} elseif (null !== $query->getCustomer()) {
|
||||||
$qb->join('t.project', 'p');
|
|
||||||
$qb->andWhere('p.customer = :customer')
|
$qb->andWhere('p.customer = :customer')
|
||||||
->setParameter('customer', $query->getCustomer());
|
->setParameter('customer', $query->getCustomer());
|
||||||
}
|
}
|
||||||
@@ -551,6 +612,8 @@ class TimesheetRepository extends EntityRepository
|
|||||||
->setParameter('tags', $query->getTags());
|
->setParameter('tags', $query->getTags());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||||
|
|
||||||
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
$qb->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Repository\Query\UserFormTypeQuery;
|
||||||
use App\Repository\Query\UserQuery;
|
use App\Repository\Query\UserQuery;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||||
|
|
||||||
class UserRepository extends EntityRepository implements UserLoaderInterface
|
class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||||
@@ -26,12 +28,28 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Used to fetch the currently logged-in user.
|
||||||
|
*
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return null|User
|
* @return null|User
|
||||||
*/
|
*/
|
||||||
public function getUserById($id): ?User
|
public function getUserById($id): ?User
|
||||||
{
|
{
|
||||||
return $this->find($id);
|
try {
|
||||||
|
return $this->createQueryBuilder('u')
|
||||||
|
->select('u', 'p', 't', 'tu', 'tl')
|
||||||
|
->leftJoin('u.preferences', 'p')
|
||||||
|
->leftJoin('u.teams', 't')
|
||||||
|
->leftJoin('t.users', 'tu')
|
||||||
|
->leftJoin('t.teamlead', 'tl')
|
||||||
|
->where('u.id = :id')
|
||||||
|
->setParameter('id', $id)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleResult();
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,12 +121,49 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
|||||||
public function loadUserByUsername($username)
|
public function loadUserByUsername($username)
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('u')
|
return $this->createQueryBuilder('u')
|
||||||
->select('u', 'p')
|
->select('u', 'p', 't', 'tu', 'tl')
|
||||||
->leftJoin('u.preferences', 'p')
|
->leftJoin('u.preferences', 'p')
|
||||||
|
->leftJoin('u.teams', 't')
|
||||||
|
->leftJoin('t.users', 'tu')
|
||||||
|
->leftJoin('t.teamlead', 'tl')
|
||||||
->where('u.username = :username')
|
->where('u.username = :username')
|
||||||
->orWhere('u.email = :username')
|
->orWhere('u.email = :username')
|
||||||
->setParameter('username', $username)
|
->setParameter('username', $username)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getSingleResult();
|
->getSingleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getQueryBuilderForFormType(UserFormTypeQuery $query): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('u');
|
||||||
|
|
||||||
|
$qb->andWhere($qb->expr()->eq('u.enabled', ':enabled'));
|
||||||
|
$qb->setParameter('enabled', true, \PDO::PARAM_BOOL);
|
||||||
|
|
||||||
|
$qb->orderBy('u.username', 'ASC');
|
||||||
|
|
||||||
|
$this->addPermissionCriteria($qb, $query->getUser(), $query->getTeams());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||||
|
{
|
||||||
|
// make sure that all queries without a user see all user
|
||||||
|
if (null === $user && empty($teams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that admins see all user
|
||||||
|
if (null !== $user && ($user->isSuperAdmin() || $user->isAdmin())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $user) {
|
||||||
|
$qb->leftJoin('u.teams', 'teams')
|
||||||
|
->leftJoin('teams.users', 'users')
|
||||||
|
->andWhere('teams.teamlead = :id')
|
||||||
|
->setParameter('id', $user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,20 @@ use App\Entity\User;
|
|||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
|
||||||
class CurrentUser
|
final class CurrentUser
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var TokenStorageInterface
|
* @var TokenStorageInterface
|
||||||
*/
|
*/
|
||||||
protected $storage;
|
private $storage;
|
||||||
/**
|
/**
|
||||||
* @var UserRepository
|
* @var UserRepository
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
private $repository;
|
||||||
|
/**
|
||||||
|
* @var User|null
|
||||||
|
*/
|
||||||
|
private $user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param TokenStorageInterface $storage
|
* @param TokenStorageInterface $storage
|
||||||
@@ -43,6 +47,11 @@ class CurrentUser
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// some inline caching to prevent multiple DB lookups
|
||||||
|
if (null !== $this->user) {
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->storage->getToken()->getUser();
|
$user = $this->storage->getToken()->getUser();
|
||||||
|
|
||||||
@@ -50,6 +59,8 @@ class CurrentUser
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->repository->getUserById($user->getId());
|
$this->user = $this->repository->getUserById($user->getId());
|
||||||
|
|
||||||
|
return $this->user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ final class IconExtension extends AbstractExtension
|
|||||||
'stop-small' => 'far fa-stop-circle',
|
'stop-small' => 'far fa-stop-circle',
|
||||||
'timesheet' => 'fas fa-clock',
|
'timesheet' => 'fas fa-clock',
|
||||||
'trash' => 'far fa-trash-alt',
|
'trash' => 'far fa-trash-alt',
|
||||||
'user' => 'fas fa-users',
|
'team' => 'fas fa-users',
|
||||||
|
'user' => 'fas fa-user-friends',
|
||||||
'visibility' => 'far fa-eye',
|
'visibility' => 'far fa-eye',
|
||||||
'settings' => 'fas fa-cog',
|
'settings' => 'fas fa-cog',
|
||||||
'export' => 'fas fa-file-export',
|
'export' => 'fas fa-file-export',
|
||||||
@@ -64,6 +65,7 @@ final class IconExtension extends AbstractExtension
|
|||||||
'profile' => 'fas fa-user-edit',
|
'profile' => 'fas fa-user-edit',
|
||||||
'warning' => 'fas fa-exclamation-triangle',
|
'warning' => 'fas fa-exclamation-triangle',
|
||||||
'permissions' => 'fas fa-user-lock',
|
'permissions' => 'fas fa-user-lock',
|
||||||
|
'unlocked' => 'fas fa-unlock-alt',
|
||||||
'back' => 'fas fa-long-arrow-alt-left',
|
'back' => 'fas fa-long-arrow-alt-left',
|
||||||
'tag' => 'fas fa-tags',
|
'tag' => 'fas fa-tags',
|
||||||
'avatar' => 'fas fa-user',
|
'avatar' => 'fas fa-user',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Voter;
|
namespace App\Voter;
|
||||||
|
|
||||||
use App\Entity\Activity;
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
@@ -65,6 +66,44 @@ class ActivityVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $attribute . '_activity');
|
if ($this->hasRolePermission($user, $attribute . '_activity')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$project = $subject->getProject();
|
||||||
|
if (null === $project) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_activity');
|
||||||
|
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_activity');
|
||||||
|
|
||||||
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Team $team */
|
||||||
|
foreach ($project->getTeams() as $team) {
|
||||||
|
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Team $team */
|
||||||
|
foreach ($project->getCustomer()->getTeams() as $team) {
|
||||||
|
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Voter;
|
namespace App\Voter;
|
||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class CustomerVoter extends AbstractVoter
|
|||||||
public const EDIT = 'edit';
|
public const EDIT = 'edit';
|
||||||
public const BUDGET = 'budget';
|
public const BUDGET = 'budget';
|
||||||
public const DELETE = 'delete';
|
public const DELETE = 'delete';
|
||||||
|
public const PERMISSIONS = 'permissions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* support rules based on the given $subject (here: Customer)
|
* support rules based on the given $subject (here: Customer)
|
||||||
@@ -31,6 +33,7 @@ class CustomerVoter extends AbstractVoter
|
|||||||
self::EDIT,
|
self::EDIT,
|
||||||
self::BUDGET,
|
self::BUDGET,
|
||||||
self::DELETE,
|
self::DELETE,
|
||||||
|
self::PERMISSIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +68,28 @@ class CustomerVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $attribute . '_customer');
|
if ($this->hasRolePermission($user, $attribute . '_customer')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_customer');
|
||||||
|
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_customer');
|
||||||
|
|
||||||
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Team $team */
|
||||||
|
foreach ($subject->getTeams() as $team) {
|
||||||
|
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Voter;
|
namespace App\Voter;
|
||||||
|
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class ProjectVoter extends AbstractVoter
|
|||||||
public const EDIT = 'edit';
|
public const EDIT = 'edit';
|
||||||
public const BUDGET = 'budget';
|
public const BUDGET = 'budget';
|
||||||
public const DELETE = 'delete';
|
public const DELETE = 'delete';
|
||||||
|
public const PERMISSIONS = 'permissions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* support rules based on the given $subject (here: Project)
|
* support rules based on the given $subject (here: Project)
|
||||||
@@ -31,6 +33,7 @@ class ProjectVoter extends AbstractVoter
|
|||||||
self::EDIT,
|
self::EDIT,
|
||||||
self::BUDGET,
|
self::BUDGET,
|
||||||
self::DELETE,
|
self::DELETE,
|
||||||
|
self::PERMISSIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +68,39 @@ class ProjectVoter extends AbstractVoter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->hasRolePermission($user, $attribute . '_project');
|
if ($this->hasRolePermission($user, $attribute . '_project')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasTeamleadPermission = $this->hasRolePermission($user, $attribute . '_teamlead_project');
|
||||||
|
$hasTeamPermission = $this->hasRolePermission($user, $attribute . '_team_project');
|
||||||
|
|
||||||
|
if (!$hasTeamleadPermission && !$hasTeamPermission) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Team $team */
|
||||||
|
foreach ($subject->getTeams() as $team) {
|
||||||
|
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Team $team */
|
||||||
|
foreach ($subject->getCustomer()->getTeams() as $team) {
|
||||||
|
if ($hasTeamleadPermission && $user->isTeamleadOf($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasTeamPermission && $user->isInTeam($team)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/Voter/TeamVoter.php
Normal file
65
src/Voter/TeamVoter.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?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\Voter;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
|
||||||
|
class TeamVoter extends AbstractVoter
|
||||||
|
{
|
||||||
|
public const VIEW = 'view';
|
||||||
|
public const EDIT = 'edit';
|
||||||
|
public const DELETE = 'delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* support rules based on the given $subject (here: Team)
|
||||||
|
*/
|
||||||
|
public const ALLOWED_ATTRIBUTES = [
|
||||||
|
self::VIEW,
|
||||||
|
self::EDIT,
|
||||||
|
self::DELETE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $attribute
|
||||||
|
* @param Team $subject
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function supports($attribute, $subject)
|
||||||
|
{
|
||||||
|
if (!($subject instanceof Team)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $attribute
|
||||||
|
* @param Team $subject
|
||||||
|
* @param TokenInterface $token
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
|
||||||
|
{
|
||||||
|
$user = $token->getUser();
|
||||||
|
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->hasRolePermission($user, $attribute . '_team');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ class UserVoter extends AbstractVoter
|
|||||||
public const DELETE = 'delete';
|
public const DELETE = 'delete';
|
||||||
public const PASSWORD = 'password';
|
public const PASSWORD = 'password';
|
||||||
public const ROLES = 'roles';
|
public const ROLES = 'roles';
|
||||||
|
public const TEAMS = 'teams';
|
||||||
public const PREFERENCES = 'preferences';
|
public const PREFERENCES = 'preferences';
|
||||||
public const API_TOKEN = 'api-token';
|
public const API_TOKEN = 'api-token';
|
||||||
public const HOURLY_RATE = 'hourly-rate';
|
public const HOURLY_RATE = 'hourly-rate';
|
||||||
@@ -30,6 +31,7 @@ class UserVoter extends AbstractVoter
|
|||||||
self::VIEW,
|
self::VIEW,
|
||||||
self::EDIT,
|
self::EDIT,
|
||||||
self::ROLES,
|
self::ROLES,
|
||||||
|
self::TEAMS,
|
||||||
self::PASSWORD,
|
self::PASSWORD,
|
||||||
self::DELETE,
|
self::DELETE,
|
||||||
self::PREFERENCES,
|
self::PREFERENCES,
|
||||||
@@ -93,6 +95,7 @@ class UserVoter extends AbstractVoter
|
|||||||
case self::PASSWORD:
|
case self::PASSWORD:
|
||||||
case self::API_TOKEN:
|
case self::API_TOKEN:
|
||||||
case self::ROLES:
|
case self::ROLES:
|
||||||
|
case self::TEAMS:
|
||||||
case self::HOURLY_RATE:
|
case self::HOURLY_RATE:
|
||||||
$permission .= $attribute;
|
$permission .= $attribute;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -38,13 +38,13 @@
|
|||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||||
{% if entry.project and entry.project.customer %}
|
{% if entry.project and entry.project.customer %}
|
||||||
{# only none-global activities have a project and customer assigned #}
|
{# only none-global activities have a project and customer assigned #}
|
||||||
{{ widgets.label_customer(entry.project.customer, path('admin_customer_edit', {'id' : entry.project.customer.id})) }}
|
{{ widgets.label_customer(entry.project.customer) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
||||||
{% if entry.project %}
|
{% if entry.project %}
|
||||||
{# only none-global activities have a project and customer assigned #}
|
{# only none-global activities have a project and customer assigned #}
|
||||||
{{ widgets.label_project(entry.project, path('admin_project_edit', {'id' : entry.project.id})) }}
|
{{ widgets.label_project(entry.project) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
'comment': 'hidden-xs',
|
'comment': 'hidden-xs',
|
||||||
'country': 'hidden-xs',
|
'country': 'hidden-xs',
|
||||||
'number': 'hidden-xs',
|
'number': 'hidden-xs',
|
||||||
|
'team': '',
|
||||||
'visible': 'hidden-xs',
|
'visible': 'hidden-xs',
|
||||||
'actions': 'actions alwaysVisible',
|
'actions': 'actions alwaysVisible',
|
||||||
} %}
|
} %}
|
||||||
@@ -37,6 +38,13 @@
|
|||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'number') }}">{{ entry.number }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">
|
||||||
|
{% if entry.teams|length > 0 %}
|
||||||
|
{{ widgets.badge_counter(entry.teams|length) }}
|
||||||
|
{% else %}
|
||||||
|
{{ widgets.icon('unlocked') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
{{ actions.customer(entry, 'index') }}
|
{{ actions.customer(entry, 'index') }}
|
||||||
|
|||||||
15
templates/customer/permissions.html.twig
Normal file
15
templates/customer/permissions.html.twig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/actions.html.twig" as actions %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'admin_customer.title'|trans }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'admin_customer.subtitle'|trans }}{% endblock %}
|
||||||
|
{% block page_actions %}{{ actions.customer(customer, 'permissions') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||||
|
'title': customer.name,
|
||||||
|
'form': form,
|
||||||
|
'back': path('admin_customer')
|
||||||
|
}) }}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
|
{{ form_start(form) }}
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border">
|
||||||
<h3 class="box-title">
|
<h3 class="box-title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -7,7 +8,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{{ form_start(form) }}
|
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
{% if back|default(false) %}
|
{% if back|default(false) %}
|
||||||
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
<a href="{{ back }}" class="btn btn-link">{{ 'action.back'|trans }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if reset|default(true) %}
|
||||||
|
<input type="reset" value="{{ 'action.reset'|trans }}" class="btn btn-link pull-right" />
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -145,6 +145,9 @@
|
|||||||
{% if is_granted('budget', project) %}
|
{% if is_granted('budget', project) %}
|
||||||
{% set actions = actions|merge({'report': {'url': path('admin_project_budget', {'id': project.id})}}) %}
|
{% set actions = actions|merge({'report': {'url': path('admin_project_budget', {'id': project.id})}}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_granted('permissions', project) %}
|
||||||
|
{% set actions = actions|merge({'permissions': {'url': path('admin_project_permissions', {'id': project.id})}}) %}
|
||||||
|
{% endif %}
|
||||||
{% if is_granted('view_activity') %}
|
{% if is_granted('view_activity') %}
|
||||||
{% set actions = actions|merge({'activity': path('admin_activity', {'customer': project.customer.id, 'project': project.id})}) %}
|
{% set actions = actions|merge({'activity': path('admin_activity', {'customer': project.customer.id, 'project': project.id})}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -198,6 +201,9 @@
|
|||||||
{% if is_granted('budget', customer) %}
|
{% if is_granted('budget', customer) %}
|
||||||
{% set actions = actions|merge({'report': {'url': path('admin_customer_budget', {'id': customer.id})}}) %}
|
{% set actions = actions|merge({'report': {'url': path('admin_customer_budget', {'id': customer.id})}}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_granted('permissions', customer) %}
|
||||||
|
{% set actions = actions|merge({'permissions': {'url': path('admin_customer_permissions', {'id': customer.id})}}) %}
|
||||||
|
{% endif %}
|
||||||
{% if is_granted('view_project') %}
|
{% if is_granted('view_project') %}
|
||||||
{% set actions = actions|merge({'project': path('admin_project', {'customer': customer.id})}) %}
|
{% set actions = actions|merge({'project': path('admin_project', {'customer': customer.id})}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -443,3 +449,40 @@
|
|||||||
{{ widgets.page_actions(actions) }}
|
{{ widgets.page_actions(actions) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro teams(view) %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
|
{% set actions = {} %}
|
||||||
|
{% if is_granted('create_team') %}
|
||||||
|
{% set actions = actions|merge({'create': {'url': path('admin_team_create')}}) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set event = trigger('actions.teams', {'actions': actions, 'view': view}) %}
|
||||||
|
{{ widgets.page_actions(event.payload.actions) }}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro team(team, view) %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
|
{% set actions = {} %}
|
||||||
|
{% if team.id is not empty %}
|
||||||
|
{% if is_granted('edit', team) %}
|
||||||
|
{% set class = '' %}
|
||||||
|
{% if view != 'edit' %}
|
||||||
|
{% set class = 'modal-ajax-form' %}
|
||||||
|
{% endif %}
|
||||||
|
{% set actions = actions|merge({'edit': {'url': path('admin_team_edit', {'id': team.id}), 'class': class}}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if view == 'index' and is_granted('delete', team) %}
|
||||||
|
{% set actions = actions|merge({'trash': {'url': path('delete_team', {'id' : team.id}), 'class': 'api-link', 'attr': {'data-event': 'kimai.teamDelete kimai.teamUpdate', 'data-method': 'DELETE', 'data-question': 'confirm.delete', 'data-msg-error': 'action.delete.error', 'data-msg-success': 'action.delete.success'}}}) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set event = trigger('actions.team', {'actions': actions, 'view': view, 'team': team}) %}
|
||||||
|
{% if view == 'index' %}
|
||||||
|
{{ widgets.table_actions(event.payload.actions) }}
|
||||||
|
{% else %}
|
||||||
|
{{ widgets.entity_actions(event.payload.actions) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
{% import _self as macro %}
|
{% import _self as macro %}
|
||||||
{% if role == 'ROLE_SUPER_ADMIN' %}
|
{% if role == 'ROLE_SUPER_ADMIN' %}
|
||||||
{{ macro.label(role, 'danger') }}
|
{{ macro.label(role, 'danger') }}
|
||||||
|
{% elseif role == 'ROLE_ADMIN' %}
|
||||||
|
{{ macro.label(role, 'warning') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ macro.label(role, 'primary') }}
|
{{ macro.label(role, 'primary') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
'name': 'alwaysVisible',
|
'name': 'alwaysVisible',
|
||||||
'customer': 'hidden-xs',
|
'customer': 'hidden-xs',
|
||||||
'comment': 'hidden-xs hidden-sm',
|
'comment': 'hidden-xs hidden-sm',
|
||||||
|
'team': '',
|
||||||
'visible': '',
|
'visible': '',
|
||||||
'actions': 'actions alwaysVisible',
|
'actions': 'actions alwaysVisible',
|
||||||
} %}
|
} %}
|
||||||
@@ -34,9 +35,16 @@
|
|||||||
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_project_edit', {'id': entry.id}) }}"{% endif %}>
|
<tr{% if is_granted('edit', entry) %} class="modal-ajax-form open-edit" data-href="{{ path('admin_project_edit', {'id': entry.id}) }}"{% endif %}>
|
||||||
<td>{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
|
<td>{{ widgets.label_color_dot('project', true, entry.name, null, entry.color) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">
|
||||||
{{ widgets.label_customer(entry.customer, path('admin_customer_edit', {'id' : entry.customer.id})) }}
|
{{ widgets.label_customer(entry.customer) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment|comment2html }}</td>
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'team') }}">
|
||||||
|
{% if entry.teams|length > 0 %}
|
||||||
|
{{ widgets.badge_counter(entry.teams|length) }}
|
||||||
|
{% else %}
|
||||||
|
{{ widgets.icon('unlocked') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'visible') }}">{{ widgets.label_visible(entry.visible) }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
{{ actions.project(entry, 'index') }}
|
{{ actions.project(entry, 'index') }}
|
||||||
|
|||||||
15
templates/project/permissions.html.twig
Normal file
15
templates/project/permissions.html.twig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/actions.html.twig" as actions %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'admin_project.title'|trans }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'admin_project.subtitle'|trans }}{% endblock %}
|
||||||
|
{% block page_actions %}{{ actions.project(project, 'permissions') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{{ include(app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||||
|
'title': project.name,
|
||||||
|
'form': form,
|
||||||
|
'back': path('admin_project')
|
||||||
|
}) }}
|
||||||
|
{% endblock %}
|
||||||
34
templates/team/edit.html.twig
Normal file
34
templates/team/edit.html.twig
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/actions.html.twig" as actions %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'teams.title'|trans({}, 'teams') }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'teams.subtitle'|trans({}, 'teams') }}{% endblock %}
|
||||||
|
{% block page_actions %}{{ actions.team(team, 'edit') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{{ include('default/_form.html.twig', {
|
||||||
|
'title': team.name|default('create'|trans),
|
||||||
|
'form': form,
|
||||||
|
'back': path('admin_team')
|
||||||
|
}) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if not customerForm is null %}
|
||||||
|
{{ include('default/_form.html.twig', {
|
||||||
|
'title': 'teams.customer_access'|trans({}, 'teams'),
|
||||||
|
'form': customerForm,
|
||||||
|
}) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not projectForm is null %}
|
||||||
|
{{ include('default/_form.html.twig', {
|
||||||
|
'title': 'teams.project_access'|trans({}, 'teams'),
|
||||||
|
'form': projectForm,
|
||||||
|
}) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
49
templates/team/index.html.twig
Normal file
49
templates/team/index.html.twig
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/actions.html.twig" as actions %}
|
||||||
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'teams.title'|trans({}, 'teams') }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'teams.subtitle'|trans({}, 'teams') }}{% endblock %}
|
||||||
|
{% block page_actions %}{{ actions.teams('index') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main_before %}
|
||||||
|
{{ toolbar.toolbar(toolbarForm, 'collapseTeams', showFilter) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% if teams|length == 0 %}
|
||||||
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% set columns = {
|
||||||
|
'name': '',
|
||||||
|
'teamlead': '',
|
||||||
|
'user': '',
|
||||||
|
'actions': 'actions alwaysVisible',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% set tableName = 'admin_teams' %}
|
||||||
|
|
||||||
|
{{ tables.data_table_header(tableName, columns, false, 'kimai.teamUpdate') }}
|
||||||
|
{% for team in teams %}
|
||||||
|
<tr{% if is_granted('edit', team) %} class="open-edit alternative-link" data-href="{{ path('admin_team_edit', {'id': team.id}) }}"{% endif %}>
|
||||||
|
<td>{{ team.name }}</td>
|
||||||
|
<td>{{ widgets.label_user(team.teamlead) }}</td>
|
||||||
|
<td>
|
||||||
|
{% for user in team.users %}
|
||||||
|
{{ widgets.label_user(user) }}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
{{ actions.team(team, 'index') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{{ tables.data_table_footer(teams, 'admin_team_paginated') }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
102
tests/API/TeamControllerTest.php
Normal file
102
tests/API/TeamControllerTest.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?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\API;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Tests\DataFixtures\TeamFixtures;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class TeamControllerTest extends APIControllerBaseTest
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(1);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/api/teams');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCollection()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/api/teams');
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertEquals(1, count($result));
|
||||||
|
$this->assertStructure($result[0], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetEntity()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/api/teams/1');
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertStructure($result, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotFound()
|
||||||
|
{
|
||||||
|
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/2');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteActionWithUnknownTeam()
|
||||||
|
{
|
||||||
|
$this->assertEntityNotFoundForDelete(User::ROLE_ADMIN, '/api/teams/255', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/api/teams/1');
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertStructure($result);
|
||||||
|
$this->assertNotEmpty($result['id']);
|
||||||
|
$id = $result['id'];
|
||||||
|
|
||||||
|
$this->request($client, '/api/teams/' . $id, 'DELETE');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertEquals(Response::HTTP_NO_CONTENT, $client->getResponse()->getStatusCode());
|
||||||
|
$this->assertEmpty($client->getResponse()->getContent());
|
||||||
|
|
||||||
|
$this->assertEntityNotFound(User::ROLE_USER, '/api/teams/' . $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertStructure(array $result, $full = true)
|
||||||
|
{
|
||||||
|
$expectedKeys = [
|
||||||
|
'id', 'name'
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($full) {
|
||||||
|
$expectedKeys = array_merge($expectedKeys, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$actual = array_keys($result);
|
||||||
|
sort($actual);
|
||||||
|
sort($expectedKeys);
|
||||||
|
|
||||||
|
$this->assertEquals($expectedKeys, $actual, 'Team structure does not match');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,12 +26,12 @@ class ActivityControllerTest extends ControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/admin/activity/');
|
$this->assertUrlIsSecured('/admin/activity/');
|
||||||
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/activity/');
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/activity/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexAction()
|
public function testIndexAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
$this->assertAccessIsGranted($client, '/admin/activity/');
|
$this->assertAccessIsGranted($client, '/admin/activity/');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@
|
|||||||
|
|
||||||
namespace App\Tests\Controller;
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Tests\DataFixtures\CustomerFixtures;
|
use App\Tests\DataFixtures\CustomerFixtures;
|
||||||
|
use App\Tests\DataFixtures\TeamFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
use App\Tests\Mocks\CustomerTestMetaFieldSubscriberMock;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -25,12 +28,12 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/admin/customer/');
|
$this->assertUrlIsSecured('/admin/customer/');
|
||||||
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/customer/');
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/customer/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexAction()
|
public function testIndexAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
$this->assertAccessIsGranted($client, '/admin/customer/');
|
$this->assertAccessIsGranted($client, '/admin/customer/');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
}
|
}
|
||||||
@@ -111,6 +114,39 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||||||
$this->assertEquals('Test Customer 2', $editForm->get('customer_edit_form[name]')->getValue());
|
$this->assertEquals('Test Customer 2', $editForm->get('customer_edit_form[name]')->getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeamPermissionAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
/** @var Customer $customer */
|
||||||
|
$customer = $em->getRepository(Customer::class)->find(1);
|
||||||
|
self::assertEquals(0, $customer->getTeams()->count());
|
||||||
|
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(2);
|
||||||
|
$fixture->setAddCustomer(false);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/customer/1/permissions');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=customer_team_permission_form]')->form();
|
||||||
|
/** @var ChoiceFormField $team1 */
|
||||||
|
$team1 = $form->get('customer_team_permission_form[teams][0]');
|
||||||
|
$team1->tick();
|
||||||
|
/** @var ChoiceFormField $team2 */
|
||||||
|
$team2 = $form->get('customer_team_permission_form[teams][1]');
|
||||||
|
$team2->tick();
|
||||||
|
|
||||||
|
$client->submit($form);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertHasDataTable($client);
|
||||||
|
|
||||||
|
/** @var Customer $customer */
|
||||||
|
$customer = $em->getRepository(Customer::class)->find(1);
|
||||||
|
self::assertEquals(2, $customer->getTeams()->count());
|
||||||
|
}
|
||||||
|
|
||||||
public function testDeleteAction()
|
public function testDeleteAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ namespace App\Tests\Controller;
|
|||||||
use App\DataFixtures\UserFixtures;
|
use App\DataFixtures\UserFixtures;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
|
use App\Tests\DataFixtures\TeamFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
use Symfony\Bundle\FrameworkBundle\Client;
|
use Symfony\Bundle\FrameworkBundle\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +97,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#settings', '#password', '#api-token']],
|
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#settings', '#password', '#api-token']],
|
||||||
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, array_merge($userTabs, ['#roles'])],
|
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, array_merge($userTabs, ['#teams', '#roles'])],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +317,52 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$this->assertEquals(['ROLE_TEAMLEAD', 'ROLE_SUPER_ADMIN', 'ROLE_USER'], $user->getRoles());
|
$this->assertEquals(['ROLE_TEAMLEAD', 'ROLE_SUPER_ADMIN', 'ROLE_USER'], $user->getRoles());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeamsActionIsSecured()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER . '/teams');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/profile/' . UserFixtures::USERNAME_USER . '/teams');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTeamsAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(3);
|
||||||
|
$fixture->setAddCustomer(true);
|
||||||
|
$fixture->setAddUser(false);
|
||||||
|
$fixture->addUserToIgnore($user);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER . '/teams');
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
$this->assertEquals([], $user->getTeams()->toArray());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=user_teams]')->form();
|
||||||
|
/** @var ChoiceFormField $team */
|
||||||
|
$team = $form->get('user_teams[teams][0]');
|
||||||
|
$team->tick();
|
||||||
|
|
||||||
|
$client->submit($form);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode(UserFixtures::USERNAME_USER) . '/teams'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$this->assertHasFlashSuccess($client);
|
||||||
|
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $user->getTeams()->count());
|
||||||
|
}
|
||||||
|
|
||||||
public function getPreferencesTestData()
|
public function getPreferencesTestData()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ use App\Entity\Timesheet;
|
|||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Tests\DataFixtures\CustomerFixtures;
|
use App\Tests\DataFixtures\CustomerFixtures;
|
||||||
use App\Tests\DataFixtures\ProjectFixtures;
|
use App\Tests\DataFixtures\ProjectFixtures;
|
||||||
|
use App\Tests\DataFixtures\TeamFixtures;
|
||||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
|
use App\Tests\Mocks\ProjectTestMetaFieldSubscriberMock;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group integration
|
* @group integration
|
||||||
@@ -26,12 +28,12 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||||||
public function testIsSecure()
|
public function testIsSecure()
|
||||||
{
|
{
|
||||||
$this->assertUrlIsSecured('/admin/project/');
|
$this->assertUrlIsSecured('/admin/project/');
|
||||||
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/project/');
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/admin/project/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexAction()
|
public function testIndexAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
$this->assertAccessIsGranted($client, '/admin/project/');
|
$this->assertAccessIsGranted($client, '/admin/project/');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
}
|
}
|
||||||
@@ -135,6 +137,39 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||||||
$this->assertEquals('Test 2', $editForm->get('project_edit_form[name]')->getValue());
|
$this->assertEquals('Test 2', $editForm->get('project_edit_form[name]')->getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeamPermissionAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
/** @var Project $project */
|
||||||
|
$project = $em->getRepository(Project::class)->find(1);
|
||||||
|
self::assertEquals(0, $project->getTeams()->count());
|
||||||
|
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(2);
|
||||||
|
$fixture->setAddCustomer(false);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/project/1/permissions');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=project_team_permission_form]')->form();
|
||||||
|
/** @var ChoiceFormField $team1 */
|
||||||
|
$team1 = $form->get('project_team_permission_form[teams][0]');
|
||||||
|
$team1->tick();
|
||||||
|
/** @var ChoiceFormField $team2 */
|
||||||
|
$team2 = $form->get('project_team_permission_form[teams][1]');
|
||||||
|
$team2->tick();
|
||||||
|
|
||||||
|
$client->submit($form);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/project/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertHasDataTable($client);
|
||||||
|
|
||||||
|
/** @var Project $project */
|
||||||
|
$project = $em->getRepository(Project::class)->find(1);
|
||||||
|
self::assertEquals(2, $project->getTeams()->count());
|
||||||
|
}
|
||||||
|
|
||||||
public function testDeleteAction()
|
public function testDeleteAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
155
tests/Controller/TeamControllerTest.php
Normal file
155
tests/Controller/TeamControllerTest.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Tests\DataFixtures\TeamFixtures;
|
||||||
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class TeamControllerTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/admin/teams/');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_TEAMLEAD, '/admin/teams/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIndexAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(5);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/teams/');
|
||||||
|
$this->assertPageActions($client, ['create' => $this->createUrl('/admin/teams/create')]);
|
||||||
|
$this->assertHasDataTable($client);
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_admin_teams', 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/teams/create');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||||
|
|
||||||
|
$editForm = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||||
|
$this->assertEquals('', $editForm->get('team_edit_form[name]')->getValue());
|
||||||
|
$this->assertEquals('5', $editForm->get('team_edit_form[teamlead]')->getValue());
|
||||||
|
|
||||||
|
$client->submit($form, [
|
||||||
|
'team_edit_form' => [
|
||||||
|
'name' => 'Test Team',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/teams/1/edit'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertHasFlashSuccess($client);
|
||||||
|
$this->assertHasCustomerAndProjectPermissionBoxes($client);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertHasCustomerAndProjectPermissionBoxes(Client $client)
|
||||||
|
{
|
||||||
|
$content = $client->getResponse()->getContent();
|
||||||
|
$this->assertStringContainsString('Grant access to customers', $content);
|
||||||
|
$this->assertStringContainsString('Grant access to projects', $content);
|
||||||
|
$this->assertEquals(1, $client->getCrawler()->filter('form[name=team_customer_form]')->count());
|
||||||
|
$this->assertEquals(1, $client->getCrawler()->filter('form[name=team_project_form]')->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(2);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/teams/1/edit');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||||
|
$this->assertNotEmpty($form->get('team_edit_form[name]')->getValue());
|
||||||
|
$client->submit($form, [
|
||||||
|
'team_edit_form' => [
|
||||||
|
'name' => 'Test Team 2'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/teams/1/edit'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$editForm = $client->getCrawler()->filter('form[name=team_edit_form]')->form();
|
||||||
|
$this->assertEquals('Test Team 2', $editForm->get('team_edit_form[name]')->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditCustomerAccessAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|
||||||
|
/** @var EntityManager $em */
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(2);
|
||||||
|
$fixture->setAddCustomer(false);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$team = $em->getRepository(Team::class)->find(1);
|
||||||
|
self::assertEquals(0, count($team->getCustomers()));
|
||||||
|
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/teams/1/edit');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=team_customer_form]')->form();
|
||||||
|
|
||||||
|
/** @var ChoiceFormField $customer */
|
||||||
|
$customer = $form->get('team_customer_form[customers][0]');
|
||||||
|
$customer->tick();
|
||||||
|
|
||||||
|
$client->submit($form);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/teams/1/edit'));
|
||||||
|
|
||||||
|
$team = $em->getRepository(Team::class)->find(1);
|
||||||
|
self::assertEquals(1, count($team->getCustomers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditProjectAccessAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|
||||||
|
/** @var EntityManager $em */
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$fixture = new TeamFixtures();
|
||||||
|
$fixture->setAmount(2);
|
||||||
|
$fixture->setAddCustomer(false);
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$team = $em->getRepository(Team::class)->find(1);
|
||||||
|
self::assertEquals(0, count($team->getProjects()));
|
||||||
|
|
||||||
|
$this->assertAccessIsGranted($client, '/admin/teams/1/edit');
|
||||||
|
$form = $client->getCrawler()->filter('form[name=team_project_form]')->form();
|
||||||
|
|
||||||
|
/** @var ChoiceFormField $customer */
|
||||||
|
$customer = $form->get('team_project_form[projects]');
|
||||||
|
$customer->select([1]);
|
||||||
|
|
||||||
|
$client->submit($form);
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/admin/teams/1/edit'));
|
||||||
|
|
||||||
|
$team = $em->getRepository(Team::class)->find(1);
|
||||||
|
self::assertEquals(1, count($team->getProjects()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,8 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testIndexActionWithQuery()
|
public function testIndexActionWithQuery()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
// Switching the user is not allowed for TEAMLEADs but ONLLY for admin and super-admins
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
$start = new \DateTime('first day of this month');
|
$start = new \DateTime('first day of this month');
|
||||||
|
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
@@ -118,7 +119,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testCreateAction()
|
public function testCreateAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
$this->request($client, '/team/timesheet/create');
|
$this->request($client, '/team/timesheet/create');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
$fixture->setStartDate('2017-05-01');
|
$fixture->setStartDate('2017-05-01');
|
||||||
$this->importFixture($em, $fixture);
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
$this->request($client, '/team/timesheet/1/edit');
|
$this->request($client, '/team/timesheet/1/edit');
|
||||||
|
|
||||||
$response = $client->getResponse();
|
$response = $client->getResponse();
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class UserControllerTest extends ControllerBaseTest
|
|||||||
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
|
|
||||||
$expectedTabs = ['#settings', '#password', '#api-token', '#roles'];
|
$expectedTabs = ['#settings', '#password', '#api-token', '#teams', '#roles'];
|
||||||
|
|
||||||
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
|
||||||
$this->assertEquals(count($expectedTabs), $tabs->count());
|
$this->assertEquals(count($expectedTabs), $tabs->count());
|
||||||
@@ -210,6 +210,6 @@ class UserControllerTest extends ControllerBaseTest
|
|||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||||
$this->assertAccessIsGranted($client, '/admin/user/permissions');
|
$this->assertAccessIsGranted($client, '/admin/user/permissions');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 69);
|
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 84);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
144
tests/DataFixtures/TeamFixtures.php
Normal file
144
tests/DataFixtures/TeamFixtures.php
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?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\DataFixtures;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Common\Persistence\ObjectManager;
|
||||||
|
use Faker\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the sample data to load in during controller tests.
|
||||||
|
*/
|
||||||
|
class TeamFixtures extends Fixture
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $amount = 0;
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $addCustomer = true;
|
||||||
|
/**
|
||||||
|
* @var User[]
|
||||||
|
*/
|
||||||
|
protected $skipUser = [];
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $addUser = true;
|
||||||
|
|
||||||
|
public function setAddCustomer(bool $useCustomer)
|
||||||
|
{
|
||||||
|
$this->addCustomer = $useCustomer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAddUser(bool $useUser)
|
||||||
|
{
|
||||||
|
$this->addUser = $useUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAmount(): int
|
||||||
|
{
|
||||||
|
return $this->amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAmount(int $amount): TeamFixtures
|
||||||
|
{
|
||||||
|
$this->amount = $amount;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUserToIgnore(User $user)
|
||||||
|
{
|
||||||
|
$this->skipUser[] = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function load(ObjectManager $manager)
|
||||||
|
{
|
||||||
|
$faker = Factory::create();
|
||||||
|
$user = $this->getAllUsers($manager);
|
||||||
|
$customer = $this->getAllCustomers($manager);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $this->amount; $i++) {
|
||||||
|
$lead = null;
|
||||||
|
while (null === $lead) {
|
||||||
|
$tmp = $user[array_rand($user)];
|
||||||
|
if (!in_array($tmp, $this->skipUser)) {
|
||||||
|
$lead = $tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = new Team();
|
||||||
|
$entity
|
||||||
|
->setName($faker->name)
|
||||||
|
->setTeamLead($lead)
|
||||||
|
;
|
||||||
|
|
||||||
|
if ($this->addUser) {
|
||||||
|
$userToAdd = null;
|
||||||
|
while (null === $userToAdd) {
|
||||||
|
$tmp = $user[array_rand($user)];
|
||||||
|
if (!in_array($tmp, $this->skipUser)) {
|
||||||
|
$userToAdd = $tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$entity->addUser($userToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->addCustomer) {
|
||||||
|
$entity->addCustomer($customer[array_rand($customer)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->persist($entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectManager $manager
|
||||||
|
* @return Customer[]
|
||||||
|
*/
|
||||||
|
protected function getAllCustomers(ObjectManager $manager)
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
/* @var Customer[] $entries */
|
||||||
|
$entries = $manager->getRepository(Customer::class)->findAll();
|
||||||
|
foreach ($entries as $temp) {
|
||||||
|
$all[$temp->getId()] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ObjectManager $manager
|
||||||
|
* @return User[]
|
||||||
|
*/
|
||||||
|
protected function getAllUsers(ObjectManager $manager)
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
/* @var User[] $entries */
|
||||||
|
$entries = $manager->getRepository(User::class)->findAll();
|
||||||
|
foreach ($entries as $temp) {
|
||||||
|
$all[$temp->getId()] = $temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,7 @@ class AppExtensionTest extends TestCase
|
|||||||
'timezone' => null,
|
'timezone' => null,
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'theme' => null,
|
'theme' => null,
|
||||||
|
'currency' => 'EUR',
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Tests\Entity;
|
|||||||
|
|
||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\CustomerMeta;
|
use App\Entity\CustomerMeta;
|
||||||
|
use App\Entity\Team;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ class CustomerTest extends TestCase
|
|||||||
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
$this->assertEquals(0, $sut->getMetaFields()->count());
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
$this->assertNull($sut->getMetaField('foo'));
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getTeams());
|
||||||
|
$this->assertEquals(0, $sut->getTeams()->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter()
|
||||||
@@ -127,4 +130,23 @@ class CustomerTest extends TestCase
|
|||||||
self::assertEquals(3, $sut->getMetaFields()->count());
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
self::assertCount(2, $sut->getVisibleMetaFields());
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeams()
|
||||||
|
{
|
||||||
|
$sut = new Customer();
|
||||||
|
$team = new Team();
|
||||||
|
self::assertEmpty($sut->getTeams());
|
||||||
|
self::assertEmpty($team->getCustomers());
|
||||||
|
|
||||||
|
$sut->addTeam($team);
|
||||||
|
self::assertCount(1, $sut->getTeams());
|
||||||
|
self::assertCount(1, $team->getCustomers());
|
||||||
|
self::assertSame($team, $sut->getTeams()[0]);
|
||||||
|
self::assertSame($sut, $team->getCustomers()[0]);
|
||||||
|
|
||||||
|
$sut->removeTeam(new Team());
|
||||||
|
$sut->removeTeam($team);
|
||||||
|
self::assertCount(0, $sut->getTeams());
|
||||||
|
self::assertCount(0, $team->getCustomers());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace App\Tests\Entity;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\ProjectMeta;
|
use App\Entity\ProjectMeta;
|
||||||
|
use App\Entity\Team;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ class ProjectTest extends TestCase
|
|||||||
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
$this->assertInstanceOf(Collection::class, $sut->getMetaFields());
|
||||||
$this->assertEquals(0, $sut->getMetaFields()->count());
|
$this->assertEquals(0, $sut->getMetaFields()->count());
|
||||||
$this->assertNull($sut->getMetaField('foo'));
|
$this->assertNull($sut->getMetaField('foo'));
|
||||||
|
$this->assertInstanceOf(Collection::class, $sut->getTeams());
|
||||||
|
$this->assertEquals(0, $sut->getTeams()->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetterAndGetter()
|
public function testSetterAndGetter()
|
||||||
@@ -101,4 +104,22 @@ class ProjectTest extends TestCase
|
|||||||
self::assertEquals(3, $sut->getMetaFields()->count());
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
self::assertCount(2, $sut->getVisibleMetaFields());
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeams()
|
||||||
|
{
|
||||||
|
$sut = new Project();
|
||||||
|
$team = new Team();
|
||||||
|
self::assertEmpty($sut->getTeams());
|
||||||
|
self::assertEmpty($team->getProjects());
|
||||||
|
|
||||||
|
$sut->addTeam($team);
|
||||||
|
self::assertCount(1, $sut->getTeams());
|
||||||
|
self::assertCount(1, $team->getProjects());
|
||||||
|
self::assertSame($team, $sut->getTeams()[0]);
|
||||||
|
self::assertSame($sut, $team->getProjects()[0]);
|
||||||
|
|
||||||
|
$sut->removeTeam($team);
|
||||||
|
self::assertCount(0, $sut->getTeams());
|
||||||
|
self::assertCount(0, $team->getProjects());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
tests/Entity/TeamTest.php
Normal file
108
tests/Entity/TeamTest.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?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\Customer;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\Team
|
||||||
|
*/
|
||||||
|
class TeamTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDefaultValues()
|
||||||
|
{
|
||||||
|
$sut = new Team();
|
||||||
|
self::assertNull($sut->getId());
|
||||||
|
self::assertNull($sut->getName());
|
||||||
|
self::assertNull($sut->getTeamLead());
|
||||||
|
self::assertInstanceOf(Collection::class, $sut->getUsers());
|
||||||
|
self::assertEquals(0, $sut->getUsers()->count());
|
||||||
|
self::assertInstanceOf(Collection::class, $sut->getCustomers());
|
||||||
|
self::assertEquals(0, $sut->getCustomers()->count());
|
||||||
|
self::assertInstanceOf(Collection::class, $sut->getProjects());
|
||||||
|
self::assertEquals(0, $sut->getProjects()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetterAndGetter()
|
||||||
|
{
|
||||||
|
$sut = new Team();
|
||||||
|
self::assertInstanceOf(Team::class, $sut->setName('foo-bar'));
|
||||||
|
self::assertEquals('foo-bar', $sut->getName());
|
||||||
|
self::assertEquals('foo-bar', (string) $sut);
|
||||||
|
|
||||||
|
$user = (new User())->setAlias('Foo!');
|
||||||
|
self::assertInstanceOf(Team::class, $sut->setTeamLead($user));
|
||||||
|
self::assertSame($user, $sut->getTeamLead());
|
||||||
|
|
||||||
|
self::assertFalse($sut->isTeamlead(new User()));
|
||||||
|
self::assertTrue($sut->isTeamlead($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCustomer()
|
||||||
|
{
|
||||||
|
$customer = new Customer();
|
||||||
|
$customer->setName('foo');
|
||||||
|
self::assertEmpty($customer->getTeams());
|
||||||
|
|
||||||
|
$sut = new Team();
|
||||||
|
$sut->addCustomer($customer);
|
||||||
|
self::assertEquals(1, $sut->getCustomers()->count());
|
||||||
|
$actual = $sut->getCustomers()[0];
|
||||||
|
self::assertSame($actual, $customer);
|
||||||
|
self::assertSame($sut, $customer->getTeams()[0]);
|
||||||
|
$sut->removeCustomer(new Customer());
|
||||||
|
self::assertEquals(1, $sut->getCustomers()->count());
|
||||||
|
$sut->removeCustomer($customer);
|
||||||
|
self::assertEquals(0, $sut->getCustomers()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProject()
|
||||||
|
{
|
||||||
|
$project = new Project();
|
||||||
|
$project->setName('foo');
|
||||||
|
self::assertEmpty($project->getTeams());
|
||||||
|
|
||||||
|
$sut = new Team();
|
||||||
|
$sut->addProject($project);
|
||||||
|
self::assertEquals(1, $sut->getProjects()->count());
|
||||||
|
$actual = $sut->getProjects()[0];
|
||||||
|
self::assertSame($actual, $project);
|
||||||
|
self::assertSame($sut, $project->getTeams()[0]);
|
||||||
|
$sut->removeProject(new Project());
|
||||||
|
self::assertEquals(1, $sut->getProjects()->count());
|
||||||
|
$sut->removeProject($project);
|
||||||
|
self::assertEquals(0, $sut->getProjects()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsers()
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$user->setAlias('foo');
|
||||||
|
self::assertEmpty($user->getTeams());
|
||||||
|
|
||||||
|
$sut = new Team();
|
||||||
|
$sut->addUser($user);
|
||||||
|
self::assertEquals(1, $sut->getUsers()->count());
|
||||||
|
$actual = $sut->getUsers()[0];
|
||||||
|
self::assertSame($actual, $user);
|
||||||
|
self::assertSame($sut, $user->getTeams()[0]);
|
||||||
|
self::assertFalse($sut->hasUser(new User()));
|
||||||
|
self::assertTrue($sut->hasUser($user));
|
||||||
|
$sut->removeUser(new User());
|
||||||
|
self::assertEquals(1, $sut->getUsers()->count());
|
||||||
|
$sut->removeUser($user);
|
||||||
|
self::assertEquals(0, $sut->getUsers()->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Tests\Entity;
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\UserPreference;
|
use App\Entity\UserPreference;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
@@ -97,4 +98,44 @@ class UserTest extends TestCase
|
|||||||
|
|
||||||
$this->assertEquals('fr', $sut->getLocale());
|
$this->assertEquals('fr', $sut->getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTeams()
|
||||||
|
{
|
||||||
|
$sut = new User();
|
||||||
|
$team = new Team();
|
||||||
|
self::assertEmpty($sut->getTeams());
|
||||||
|
self::assertEmpty($team->getUsers());
|
||||||
|
|
||||||
|
$sut->addTeam($team);
|
||||||
|
self::assertCount(1, $sut->getTeams());
|
||||||
|
self::assertSame($team, $sut->getTeams()[0]);
|
||||||
|
self::assertSame($sut, $team->getUsers()[0]);
|
||||||
|
|
||||||
|
self::assertFalse($sut->isTeamleadOf($team));
|
||||||
|
self::assertTrue($sut->isInTeam($team));
|
||||||
|
|
||||||
|
$team2 = new Team();
|
||||||
|
self::assertFalse($sut->isInTeam($team2));
|
||||||
|
self::assertFalse($sut->isTeamleadOf($team2));
|
||||||
|
$team2->setTeamLead($sut);
|
||||||
|
self::assertTrue($sut->isTeamleadOf($team2));
|
||||||
|
self::assertTrue($sut->isInTeam($team2));
|
||||||
|
|
||||||
|
$sut->removeTeam(new Team());
|
||||||
|
self::assertCount(2, $sut->getTeams());
|
||||||
|
$sut->removeTeam($team);
|
||||||
|
self::assertCount(1, $sut->getTeams());
|
||||||
|
$sut->removeTeam($team2);
|
||||||
|
self::assertCount(0, $sut->getTeams());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRoles()
|
||||||
|
{
|
||||||
|
$sut = new User();
|
||||||
|
self::assertFalse($sut->isTeamlead());
|
||||||
|
$sut->addRole(User::ROLE_ADMIN);
|
||||||
|
self::assertFalse($sut->isTeamlead());
|
||||||
|
$sut->addRole(User::ROLE_TEAMLEAD);
|
||||||
|
self::assertTrue($sut->isTeamlead());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user