Compare commits

13 Commits
main ... bundle

Author SHA1 Message Date
30de93b6c8 svg 2025-08-03 22:42:57 +02:00
67767310d3 svg 2025-08-03 22:42:14 +02:00
d7a36aca4c svg 2025-08-02 22:07:57 +02:00
7547e11d39 svg 2025-08-02 13:04:04 +02:00
a53f4be3e5 svg 2025-08-02 11:38:02 +02:00
4e2971c942 svg 2025-08-01 20:58:12 +02:00
81de4c1a81 svg 2025-08-01 19:13:24 +02:00
cf4ec48113 svg 2025-08-01 18:31:29 +02:00
ecf999aa76 svg 2025-08-01 18:14:21 +02:00
52984edc3c svg 2025-07-31 22:18:18 +02:00
1633c17c7b svg 2025-07-31 17:42:06 +02:00
6afe38d089 svg 2025-07-31 08:26:05 +02:00
f467b867da svg 2025-07-31 08:04:11 +02:00
27 changed files with 844 additions and 589 deletions

View File

@ -31,6 +31,9 @@ services:
- ./misc:/app/misc:delegated - ./misc:/app/misc:delegated
- ./public/lib:/app/public/lib:delegated - ./public/lib:/app/public/lib:delegated
- ./.env.local:/app/.env.local - ./.env.local:/app/.env.local
- ./vendor:/app/vendor:delegated
- ./public/bundles:/app/public/bundles:delegated
adminer: adminer:
image: adminer image: adminer

View File

@ -8,6 +8,8 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-iconv": "*", "ext-iconv": "*",
"apereo/phpcas": "^1.6", "apereo/phpcas": "^1.6",
"bnine/filesbundle": "^1.0",
"bnine/mdeditorbundle": "^1.1",
"doctrine/dbal": "^3", "doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.3",

396
composer.lock generated
View File

@ -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": "236661a47d3b0278e1198c4fb0940e3a", "content-hash": "2d59dc750783494ae1dbf58d9a3025c3",
"packages": [ "packages": [
{ {
"name": "apereo/phpcas", "name": "apereo/phpcas",
@ -77,6 +77,126 @@
}, },
"time": "2023-02-19T19:52:35+00:00" "time": "2023-02-19T19:52:35+00:00"
}, },
{
"name": "bnine/filesbundle",
"version": "v1.0.6",
"source": {
"type": "git",
"url": "https://github.com/afornerot/bNine-FilesBundle.git",
"reference": "add22ab4bd7c7342968e901294a2990b7efc8895"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/afornerot/bNine-FilesBundle/zipball/add22ab4bd7c7342968e901294a2990b7efc8895",
"reference": "add22ab4bd7c7342968e901294a2990b7efc8895",
"shasum": ""
},
"require": {
"php": "^8.1",
"symfony/framework-bundle": "^7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.85"
},
"suggest": {
"oneup/uploader-bundle": "^4.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-main": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"Bnine\\FilesBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "afornerot",
"email": "arno.nanor@gmail.com"
}
],
"description": "Symfony bundle for entity-based file browser and management.",
"keywords": [
"bundle",
"directory",
"entity",
"file-browser",
"files",
"symfony",
"upload"
],
"support": {
"issues": "https://github.com/afornerot/bNine-FilesBundle/issues",
"source": "https://github.com/afornerot/bNine-FilesBundle/tree/v1.0.6"
},
"time": "2025-08-02T11:06:27+00:00"
},
{
"name": "bnine/mdeditorbundle",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/afornerot/bNine-MdEditorBundle.git",
"reference": "9c0e29e989f68d64664c060e3b1d284abdebc9c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/afornerot/bNine-MdEditorBundle/zipball/9c0e29e989f68d64664c060e3b1d284abdebc9c5",
"reference": "9c0e29e989f68d64664c060e3b1d284abdebc9c5",
"shasum": ""
},
"require": {
"php": "^8.1",
"symfony/framework-bundle": "^7.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.85"
},
"suggest": {
"oneup/uploader-bundle": "^4.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-main": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"Bnine\\MdEditorBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "afornerot",
"email": "arno.nanor@gmail.com"
}
],
"description": "Symfony bundle for entity-based file browser and management.",
"keywords": [
"bundle",
"editor",
"entity",
"markdown",
"symfony"
],
"support": {
"issues": "https://github.com/afornerot/bNine-MdEditorBundle/issues",
"source": "https://github.com/afornerot/bNine-MdEditorBundle/tree/v1.1.4"
},
"time": "2025-08-03T20:42:31+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.13.1", "version": "0.13.1",
@ -212,99 +332,6 @@
}, },
"time": "2024-07-08T12:26:09+00:00" "time": "2024-07-08T12:26:09+00:00"
}, },
{
"name": "doctrine/cache",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb",
"reference": "1ca8f21980e770095a31456042471a57bc4c68fb",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/coding-standard": "^9",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/cache": "^4.4 || ^5.4 || ^6",
"symfony/var-exporter": "^4.4 || ^5.4 || ^6"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
"homepage": "https://www.doctrine-project.org/projects/cache.html",
"keywords": [
"abstraction",
"apcu",
"cache",
"caching",
"couchdb",
"memcached",
"php",
"redis",
"xcache"
],
"support": {
"issues": "https://github.com/doctrine/cache/issues",
"source": "https://github.com/doctrine/cache/tree/2.2.0"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
"type": "tidelift"
}
],
"time": "2022-05-20T20:07:39+00:00"
},
{ {
"name": "doctrine/collections", "name": "doctrine/collections",
"version": "2.3.0", "version": "2.3.0",
@ -393,28 +420,31 @@
}, },
{ {
"name": "doctrine/dbal", "name": "doctrine/dbal",
"version": "3.9.5", "version": "3.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/dbal.git", "url": "https://github.com/doctrine/dbal.git",
"reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868" "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/4a4e2eed3134036ee36a147ee0dac037dfa17868", "url": "https://api.github.com/repos/doctrine/dbal/zipball/1cf840d696373ea0d58ad0a8875c0fadcfc67214",
"reference": "4a4e2eed3134036ee36a147ee0dac037dfa17868", "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"composer-runtime-api": "^2", "composer-runtime-api": "^2",
"doctrine/cache": "^1.11|^2.0",
"doctrine/deprecations": "^0.5.3|^1", "doctrine/deprecations": "^0.5.3|^1",
"doctrine/event-manager": "^1|^2", "doctrine/event-manager": "^1|^2",
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"psr/cache": "^1|^2|^3", "psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3" "psr/log": "^1|^2|^3"
}, },
"conflict": {
"doctrine/cache": "< 1.11"
},
"require-dev": { "require-dev": {
"doctrine/cache": "^1.11|^2.0",
"doctrine/coding-standard": "13.0.0", "doctrine/coding-standard": "13.0.0",
"fig/log-test": "^1", "fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1", "jetbrains/phpstorm-stubs": "2023.1",
@ -484,7 +514,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/dbal/issues", "issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.9.5" "source": "https://github.com/doctrine/dbal/tree/3.10.0"
}, },
"funding": [ "funding": [
{ {
@ -500,7 +530,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-15T22:40:05+00:00" "time": "2025-07-10T21:11:04+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@ -552,16 +582,16 @@
}, },
{ {
"name": "doctrine/doctrine-bundle", "name": "doctrine/doctrine-bundle",
"version": "2.15.0", "version": "2.15.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git", "url": "https://github.com/doctrine/DoctrineBundle.git",
"reference": "d88294521a1bca943240adca65fa19ca8a7288c6" "reference": "5a305c5e776f9d3eb87f5b94d40d50aff439211d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/d88294521a1bca943240adca65fa19ca8a7288c6", "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/5a305c5e776f9d3eb87f5b94d40d50aff439211d",
"reference": "d88294521a1bca943240adca65fa19ca8a7288c6", "reference": "5a305c5e776f9d3eb87f5b94d40d50aff439211d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -654,7 +684,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues", "issues": "https://github.com/doctrine/DoctrineBundle/issues",
"source": "https://github.com/doctrine/DoctrineBundle/tree/2.15.0" "source": "https://github.com/doctrine/DoctrineBundle/tree/2.15.1"
}, },
"funding": [ "funding": [
{ {
@ -670,7 +700,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-16T19:53:58+00:00" "time": "2025-07-30T15:48:28+00:00"
}, },
{ {
"name": "doctrine/doctrine-migrations-bundle", "name": "doctrine/doctrine-migrations-bundle",
@ -1088,16 +1118,16 @@
}, },
{ {
"name": "doctrine/migrations", "name": "doctrine/migrations",
"version": "3.9.1", "version": "3.9.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/doctrine/migrations.git", "url": "https://github.com/doctrine/migrations.git",
"reference": "0f1e0c960ac29866d648a4f50142a74fe1cb6999" "reference": "fa94c6f06b1bc6d4759481ec20b8b81d13e861be"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/doctrine/migrations/zipball/0f1e0c960ac29866d648a4f50142a74fe1cb6999", "url": "https://api.github.com/repos/doctrine/migrations/zipball/fa94c6f06b1bc6d4759481ec20b8b81d13e861be",
"reference": "0f1e0c960ac29866d648a4f50142a74fe1cb6999", "reference": "fa94c6f06b1bc6d4759481ec20b8b81d13e861be",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1115,18 +1145,18 @@
"doctrine/orm": "<2.12 || >=4" "doctrine/orm": "<2.12 || >=4"
}, },
"require-dev": { "require-dev": {
"doctrine/coding-standard": "^12", "doctrine/coding-standard": "^13",
"doctrine/orm": "^2.13 || ^3", "doctrine/orm": "^2.13 || ^3",
"doctrine/persistence": "^2 || ^3 || ^4", "doctrine/persistence": "^2 || ^3 || ^4",
"doctrine/sql-formatter": "^1.0", "doctrine/sql-formatter": "^1.0",
"ext-pdo_sqlite": "*", "ext-pdo_sqlite": "*",
"fig/log-test": "^1", "fig/log-test": "^1",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-phpunit": "^1.3", "phpstan/phpstan-phpunit": "^2",
"phpstan/phpstan-strict-rules": "^1.4", "phpstan/phpstan-strict-rules": "^2",
"phpstan/phpstan-symfony": "^1.3", "phpstan/phpstan-symfony": "^2",
"phpunit/phpunit": "^10.3", "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0",
"symfony/cache": "^5.4 || ^6.0 || ^7.0", "symfony/cache": "^5.4 || ^6.0 || ^7.0",
"symfony/process": "^5.4 || ^6.0 || ^7.0", "symfony/process": "^5.4 || ^6.0 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0" "symfony/yaml": "^5.4 || ^6.0 || ^7.0"
@ -1171,7 +1201,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/doctrine/migrations/issues", "issues": "https://github.com/doctrine/migrations/issues",
"source": "https://github.com/doctrine/migrations/tree/3.9.1" "source": "https://github.com/doctrine/migrations/tree/3.9.2"
}, },
"funding": [ "funding": [
{ {
@ -1187,7 +1217,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-27T07:19:23+00:00" "time": "2025-07-29T11:36:14+00:00"
}, },
{ {
"name": "doctrine/orm", "name": "doctrine/orm",
@ -1929,16 +1959,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "2.7.0", "version": "2.7.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" "reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
"reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1967,7 +1997,7 @@
"symfony/process": "^5.4 | ^6.0 | ^7.0", "symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1", "unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0" "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
}, },
"suggest": { "suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension" "symfony/yaml": "v2.3+ required if using the Front Matter extension"
@ -2032,7 +2062,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-05-05T12:20:28+00:00" "time": "2025-07-20T12:47:49+00:00"
}, },
{ {
"name": "league/config", "name": "league/config",
@ -4657,16 +4687,16 @@
}, },
{ {
"name": "symfony/flex", "name": "symfony/flex",
"version": "v2.8.0", "version": "v2.8.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/flex.git", "url": "https://github.com/symfony/flex.git",
"reference": "68cdcde0b7e36b008a08bcf3709c07a20e757a29" "reference": "423c36e369361003dc31ef11c5f15fb589e52c01"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/flex/zipball/68cdcde0b7e36b008a08bcf3709c07a20e757a29", "url": "https://api.github.com/repos/symfony/flex/zipball/423c36e369361003dc31ef11c5f15fb589e52c01",
"reference": "68cdcde0b7e36b008a08bcf3709c07a20e757a29", "reference": "423c36e369361003dc31ef11c5f15fb589e52c01",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4705,7 +4735,7 @@
"description": "Composer plugin for Symfony", "description": "Composer plugin for Symfony",
"support": { "support": {
"issues": "https://github.com/symfony/flex/issues", "issues": "https://github.com/symfony/flex/issues",
"source": "https://github.com/symfony/flex/tree/v2.8.0" "source": "https://github.com/symfony/flex/tree/v2.8.1"
}, },
"funding": [ "funding": [
{ {
@ -4721,7 +4751,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-04T06:30:46+00:00" "time": "2025-07-05T07:45:19+00:00"
}, },
{ {
"name": "symfony/form", "name": "symfony/form",
@ -7516,16 +7546,16 @@
}, },
{ {
"name": "symfony/stimulus-bundle", "name": "symfony/stimulus-bundle",
"version": "v2.27.0", "version": "v2.28.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/stimulus-bundle.git", "url": "https://github.com/symfony/stimulus-bundle.git",
"reference": "defaeb91bd366f9f43dbe54dbdfd9bc3c4138814" "reference": "4ebef4b41e2524b7b797a103144256e5f7b39226"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/defaeb91bd366f9f43dbe54dbdfd9bc3c4138814", "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/4ebef4b41e2524b7b797a103144256e5f7b39226",
"reference": "defaeb91bd366f9f43dbe54dbdfd9bc3c4138814", "reference": "4ebef4b41e2524b7b797a103144256e5f7b39226",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7565,7 +7595,7 @@
"symfony-ux" "symfony-ux"
], ],
"support": { "support": {
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.27.0" "source": "https://github.com/symfony/stimulus-bundle/tree/v2.28.2"
}, },
"funding": [ "funding": [
{ {
@ -7576,12 +7606,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-22T19:07:55+00:00" "time": "2025-07-29T15:18:27+00:00"
}, },
{ {
"name": "symfony/stopwatch", "name": "symfony/stopwatch",
@ -8181,16 +8215,16 @@
}, },
{ {
"name": "symfony/ux-turbo", "name": "symfony/ux-turbo",
"version": "v2.27.0", "version": "v2.28.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/ux-turbo.git", "url": "https://github.com/symfony/ux-turbo.git",
"reference": "b9ce9b30a9cf9bbd090c7ad290bdaf84a0e100b2" "reference": "6094406d9cddc4bf2b583cef86c20edce1d534fa"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/b9ce9b30a9cf9bbd090c7ad290bdaf84a0e100b2", "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/6094406d9cddc4bf2b583cef86c20edce1d534fa",
"reference": "b9ce9b30a9cf9bbd090c7ad290bdaf84a0e100b2", "reference": "6094406d9cddc4bf2b583cef86c20edce1d534fa",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8260,7 +8294,7 @@
"turbo-stream" "turbo-stream"
], ],
"support": { "support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.27.0" "source": "https://github.com/symfony/ux-turbo/tree/v2.28.2"
}, },
"funding": [ "funding": [
{ {
@ -8271,12 +8305,16 @@
"url": "https://github.com/fabpot", "url": "https://github.com/fabpot",
"type": "github" "type": "github"
}, },
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-06T20:27:21+00:00" "time": "2025-07-29T15:18:27+00:00"
}, },
{ {
"name": "symfony/validator", "name": "symfony/validator",
@ -9575,16 +9613,16 @@
}, },
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
"version": "2.9.0", "version": "2.10.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Masterminds/html5-php.git", "url": "https://github.com/Masterminds/html5-php.git",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" "reference": "fcf91eb64359852f00d921887b219479b4f21251"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", "reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9636,22 +9674,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/Masterminds/html5-php/issues", "issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.9.0" "source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
}, },
"time": "2024-03-31T07:05:07+00:00" "time": "2025-07-25T09:04:22+00:00"
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.13.2", "version": "1.13.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "d25e62e636b0a9b01e3bdebb7823b474876dd829" "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/d25e62e636b0a9b01e3bdebb7823b474876dd829", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"reference": "d25e62e636b0a9b01e3bdebb7823b474876dd829", "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9690,7 +9728,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/myclabs/DeepCopy/issues", "issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.2" "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
}, },
"funding": [ "funding": [
{ {
@ -9698,20 +9736,20 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-07-04T14:07:32+00:00" "time": "2025-08-01T08:46:24+00:00"
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v5.5.0", "version": "v5.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9" "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9754,9 +9792,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/nikic/PHP-Parser/issues", "issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0"
}, },
"time": "2025-05-31T08:24:38+00:00" "time": "2025-07-27T20:03:57+00:00"
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
@ -9878,16 +9916,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "2.1.17", "version": "2.1.21",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1ccf445757458c06a04eb3f803603cb118fe5fa6",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "reference": "1ccf445757458c06a04eb3f803603cb118fe5fa6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9932,20 +9970,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-05-21T20:55:28+00:00" "time": "2025-07-28T19:35:08+00:00"
}, },
{ {
"name": "phpstan/phpstan-doctrine", "name": "phpstan/phpstan-doctrine",
"version": "2.0.3", "version": "2.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git", "url": "https://github.com/phpstan/phpstan-doctrine.git",
"reference": "4497663eb17b9d29211830df5aceaa3a4d256a35" "reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/4497663eb17b9d29211830df5aceaa3a4d256a35", "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8",
"reference": "4497663eb17b9d29211830df5aceaa3a4d256a35", "reference": "6271e66ce37545bd2edcddbe6bcbdd3b665ab7b8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10002,22 +10040,22 @@
"description": "Doctrine extensions for PHPStan", "description": "Doctrine extensions for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues", "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
"source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.3" "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.4"
}, },
"time": "2025-05-05T15:28:52+00:00" "time": "2025-07-17T11:57:55+00:00"
}, },
{ {
"name": "phpstan/phpstan-symfony", "name": "phpstan/phpstan-symfony",
"version": "2.0.6", "version": "2.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-symfony.git", "url": "https://github.com/phpstan/phpstan-symfony.git",
"reference": "5005288e07583546ea00b52de4a9ac412eb869d7" "reference": "392f7ab8f52a0a776977be4e62535358c28e1b15"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/5005288e07583546ea00b52de4a9ac412eb869d7", "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/392f7ab8f52a0a776977be4e62535358c28e1b15",
"reference": "5005288e07583546ea00b52de4a9ac412eb869d7", "reference": "392f7ab8f52a0a776977be4e62535358c28e1b15",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10073,9 +10111,9 @@
"description": "Symfony Framework extensions and rules for PHPStan", "description": "Symfony Framework extensions and rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-symfony/issues", "issues": "https://github.com/phpstan/phpstan-symfony/issues",
"source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.6" "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.7"
}, },
"time": "2025-05-14T07:00:05+00:00" "time": "2025-07-22T09:40:57+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",

View File

@ -15,4 +15,6 @@ return [
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Oneup\UploaderBundle\OneupUploaderBundle::class => ['all' => true], Oneup\UploaderBundle\OneupUploaderBundle::class => ['all' => true],
FOS\RestBundle\FOSRestBundle::class => ['all' => true], FOS\RestBundle\FOSRestBundle::class => ['all' => true],
Bnine\FilesBundle\BnineFilesBundle::class => ['all' => true],
Bnine\MdEditorBundle\BnineMdEditorBundle::class => ['all' => true],
]; ];

View File

@ -4,5 +4,5 @@ oneup_uploader:
frontend: dropzone frontend: dropzone
logo: logo:
frontend: dropzone frontend: dropzone
file: bninefile:
frontend: dropzone frontend: dropzone

View File

@ -3,6 +3,9 @@ twig:
form_themes: ['bootstrap_5_layout.html.twig'] form_themes: ['bootstrap_5_layout.html.twig']
globals: globals:
appName: "%appName%" appName: "%appName%"
paths:
'%kernel.project_dir%/vendor/bnine/filesbundle/templates': BnineFilesBundle
'%kernel.project_dir%/vendor/bnine/mdeditorbundle/templates': BnineMdEditorBundle
when@test: when@test:
twig: twig:
strict_variables: true strict_variables: true

View File

@ -3,3 +3,11 @@ controllers:
path: ../src/Controller/ path: ../src/Controller/
namespace: App\Controller namespace: App\Controller
type: attribute type: attribute
bninefilesbundle:
resource: '@BnineFilesBundle/config/routes.yaml'
prefix: '/bninefiles'
bninemdeditorbundle:
resource: '@BnineMdEditorBundle/config/routes.yaml'
prefix: '/bninemdeditor'

View File

@ -29,4 +29,3 @@ services:
App\Security\DynamicAuthenticator: App\Security\DynamicAuthenticator:
arguments: arguments:
$modeAuth: '%env(MODE_AUTH)%' $modeAuth: '%env(MODE_AUTH)%'

View File

@ -1,109 +0,0 @@
<?php
namespace App\Controller;
use App\Service\FileService;
use Oneup\UploaderBundle\Uploader\Response\ResponseInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class FileController extends AbstractController
{
private FileService $fileService;
public function __construct(FileService $fileService)
{
$this->fileService = $fileService;
}
#[Route('/user/file/{domain}/{id}/{editable}', name: 'app_files', methods: ['GET'])]
public function browse(string $domain, int $id, int $editable, Request $request): Response
{
$relativePath = $request->query->get('path', '');
try {
$files = $this->fileService->list($domain, (string) $id, $relativePath);
return $this->render('file/browse.html.twig', [
'domain' => $domain,
'id' => $id,
'files' => $files,
'path' => $relativePath,
'editable' => $editable,
]);
} catch (\Exception $e) {
$this->addFlash('danger', $e->getMessage());
dd($e->getMessage());
return $this->redirectToRoute('app_files', [
'domain' => $domain,
'id' => $id,
'editable' => $editable,
]);
}
}
#[Route('/user/uploadmodal/{domain}/{id}', name: 'app_files_uploadmodal', methods: ['GET'])]
public function uploadmodal(string $domain, int $id, Request $request): Response
{
$relativePath = $request->query->get('path', '');
return $this->render('file\upload.html.twig', [
'useheader' => false,
'usemenu' => false,
'usesidebar' => false,
'endpoint' => 'file',
'domain' => $domain,
'id' => $id,
'path' => $relativePath,
]);
}
#[Route('/user/uploadfile', name: 'app_files_uploadfile', methods: ['POST'])]
public function upload(Request $request): Response|ResponseInterface
{
/** @var UploadedFile $file */
$file = $request->files->get('file');
$domain = $request->query->get('domain');
$id = $request->query->get('id');
$relativePath = $request->query->get('path', '');
if (!$file || !$domain || !$id) {
return new Response('Invalid parameters', 400);
}
$baseDir = $this->getParameter('kernel.project_dir').'/uploads/'.$domain.'/'.$id.'/'.ltrim($relativePath, '/');
if (!is_dir($baseDir)) {
mkdir($baseDir, 0775, true);
}
$originalName = $file->getClientOriginalName();
$file->move($baseDir, $originalName);
return new JsonResponse(['success' => true]);
}
#[Route('/user/file/{domain}/{id}/delete', name: 'app_files_delete', methods: ['POST'])]
public function delete(string $domain, int $id, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$relativePath = $data['path'] ?? null;
if (!$relativePath) {
return $this->json(['error' => 'Chemin non fourni.'], 400);
}
try {
$this->fileService->delete($domain, (string) $id, $relativePath);
return $this->json(['success' => true]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 400);
}
}
}

View File

@ -6,7 +6,7 @@ use App\Entity\Project;
use App\Entity\User; use App\Entity\User;
use App\Form\ProjectType; use App\Form\ProjectType;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use App\Service\FileService; use Bnine\FilesBundle\Service\FileService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -78,7 +78,7 @@ class ProjectController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$em->flush(); $em->flush();
return $this->redirectToRoute('app_admin_project'); // return $this->redirectToRoute('app_admin_project');
} }
return $this->render('project/edit.html.twig', [ return $this->render('project/edit.html.twig', [

View File

@ -8,8 +8,18 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectRepository::class)] #[ORM\Entity(repositoryClass: ProjectRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Project class Project
{ {
public const DRAFT = 'Brouillon';
public const TOVOTE = 'A Voter';
public const VOTED = 'Voté';
public const ARCHIVED = 'Archivé';
public const NATURE_COLLECTIVE = 'Collective';
public const NATURE_STRATEGIC = 'Stratégique';
public const NATURE_TACTICAL = 'Tactique';
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
@ -18,11 +28,20 @@ class Project
#[ORM\Column(length: 255, unique: true)] #[ORM\Column(length: 255, unique: true)]
private ?string $title = null; private ?string $title = null;
#[ORM\Column()] #[ORM\Column(type: 'text')]
private int $status; private string $summary;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description = null;
#[ORM\Column(type: 'string', length: 20)]
private string $nature;
#[ORM\Column]
private string $status;
#[ORM\Column(type: 'datetime', nullable: true)] #[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $updateAt = null; private ?\DateTimeInterface $dueDate = null;
/** /**
* @var Collection<int, User> * @var Collection<int, User>
@ -30,9 +49,17 @@ class Project
#[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')] #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'projects')]
private Collection $users; private Collection $users;
/**
* @var Collection<int, ProjectTimeline>
*/
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectTimeline::class, cascade: ['remove'])]
#[ORM\OrderBy(['createdAt' => 'DESC'])]
private Collection $timelines;
public function __construct() public function __construct()
{ {
$this->users = new ArrayCollection(); $this->users = new ArrayCollection();
$this->timelines = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@ -52,30 +79,66 @@ class Project
return $this; return $this;
} }
public function getUpdateAt(): ?\DateTimeInterface public function getSummary(): string
{ {
return $this->updateAt; return $this->summary;
} }
public function setUpdateAt(?\DateTimeInterface $updateAt): static public function setSummary(string $summary): static
{ {
$this->updateAt = $updateAt; $this->summary = $summary;
return $this; return $this;
} }
public function getStatus(): ?int public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getNature(): string
{
return $this->nature;
}
public function setNature(string $nature): static
{
$this->nature = $nature;
return $this;
}
public function getStatus(): string
{ {
return $this->status; return $this->status;
} }
public function setStatus(int $status): static public function setStatus(string $status): static
{ {
$this->status = $status; $this->status = $status;
return $this; return $this;
} }
public function getDueDate(): ?\DateTimeInterface
{
return $this->dueDate;
}
public function setDueDate(?\DateTimeInterface $dueDate): static
{
$this->dueDate = $dueDate;
return $this;
}
/** /**
* @return Collection<int, User> * @return Collection<int, User>
*/ */
@ -102,4 +165,9 @@ class Project
return $this; return $this;
} }
public function getTimelines(): Collection
{
return $this->timelines;
}
} }

View File

@ -0,0 +1,86 @@
<?php
namespace App\Entity;
use App\Repository\ProjectTimelineRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectTimelineRepository::class)]
class ProjectTimeline
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'timelines')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Project $project;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private User $user;
#[ORM\Column(type: 'datetime')]
private \DateTimeInterface $createdAt;
#[ORM\Column(type: 'array')]
private array $description;
public function __construct()
{
}
public function getId(): ?int
{
return $this->id;
}
public function getProject(): Project
{
return $this->project;
}
public function setProject(Project $project): static
{
$this->project = $project;
return $this;
}
public function getUser(): User
{
return $this->user;
}
public function setUser(User $user): static
{
$this->user = $user;
return $this;
}
public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createAt): static
{
$this->createdAt = $createAt;
return $this;
}
public function getDescription(): array
{
return $this->description;
}
public function setDescription(array $description): static
{
$this->description = $description;
return $this;
}
}

View File

@ -105,6 +105,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function hasRole(string $role): bool
{
return in_array($role, $this->getRoles());
}
/** /**
* @see PasswordAuthenticatedUserInterface * @see PasswordAuthenticatedUserInterface
*/ */

View File

@ -0,0 +1,155 @@
<?php
namespace App\EventListener;
use App\Entity\Project;
use App\Entity\ProjectTimeline;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Symfony\Bundle\SecurityBundle\Security;
#[AsDoctrineListener(event: 'onFlush')]
class ProjectListener
{
public function __construct(
private readonly Security $security,
) {
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$user = $this->security->getUser();
if (!$user) {
return;
}
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Project) {
continue;
}
$timeline = new ProjectTimeline();
$timeline->setProject($entity);
$timeline->setUser($user);
$timeline->setCreatedAt(new \DateTime());
$timeline->setDescription(['created' => true]);
$em->persist($timeline);
$meta = $em->getClassMetadata(ProjectTimeline::class);
$uow->computeChangeSet($meta, $timeline);
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Project) {
continue;
}
$changeSet = $uow->getEntityChangeSet($entity);
$changes = [];
foreach ($changeSet as $field => [$old, $new]) {
$oldStr = $this->stringify($old);
$newStr = $this->stringify($new);
if ($oldStr !== $newStr) {
$changes[$field] = [
'old' => $oldStr,
'new' => $newStr,
];
}
}
if (!empty($changes)) {
$timeline = new ProjectTimeline();
$timeline->setProject($entity);
$timeline->setUser($user);
$timeline->setCreatedAt(new \DateTime());
$timeline->setDescription($changes);
$em->persist($timeline);
$meta = $em->getClassMetadata(ProjectTimeline::class);
$uow->computeChangeSet($meta, $timeline);
}
}
foreach ($uow->getScheduledCollectionUpdates() as $col) {
$owner = $col->getOwner();
if (!$owner instanceof Project) {
continue;
}
$mapping = $col->getMapping();
$fieldName = $mapping['fieldName'];
$added = $col->getInsertDiff();
$removed = $col->getDeleteDiff();
$changes = [];
foreach ($added as $addedEntity) {
$changes[$fieldName.'_added'][] = $this->stringify($addedEntity);
}
foreach ($removed as $removedEntity) {
$changes[$fieldName.'_removed'][] = $this->stringify($removedEntity);
}
if (!empty($changes)) {
$timeline = new ProjectTimeline();
$timeline->setProject($owner);
$timeline->setUser($user);
$timeline->setCreatedAt(new \DateTime());
$timeline->setDescription($changes);
$em->persist($timeline);
$meta = $em->getClassMetadata(ProjectTimeline::class);
$uow->computeChangeSet($meta, $timeline);
}
}
}
private function stringify(mixed $value): string
{
if (is_null($value)) {
return 'null';
}
if (is_scalar($value)) {
return (string) $value;
}
if ($value instanceof Collection || is_array($value)) {
$elements = [];
foreach ($value as $item) {
$elements[] = $this->stringify($item);
}
return '['.implode(', ', $elements).']';
}
if (is_object($value)) {
if (method_exists($value, '__toString')) {
return (string) $value;
}
foreach (['getUsername', 'getName', 'getTitle', 'getEmail', 'getId'] as $method) {
if (method_exists($value, $method)) {
return (string) $value->{$method}();
}
}
return get_class($value);
}
return json_encode($value);
}
}

View File

@ -4,10 +4,12 @@ namespace App\Form;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\User; use App\Entity\User;
use Bnine\MdEditorBundle\Form\Type\MarkdownType;
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\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -26,8 +28,32 @@ class ProjectType extends AbstractType
'label' => 'Titre', 'label' => 'Titre',
]) ])
->add('summary', TextareaType::class, [
'label' => 'Résumé',
])
->add('status', ChoiceType::class, [ ->add('status', ChoiceType::class, [
'choices' => ['Brouillon' => 0], 'label' => 'Statut',
'choices' => [
Project::DRAFT => Project::DRAFT,
Project::TOVOTE => Project::TOVOTE,
Project::VOTED => Project::VOTED,
Project::ARCHIVED => Project::ARCHIVED,
],
])
->add('nature', ChoiceType::class, [
'label' => 'Nature',
'choices' => [
Project::NATURE_COLLECTIVE => Project::NATURE_COLLECTIVE,
Project::NATURE_STRATEGIC => Project::NATURE_STRATEGIC,
Project::NATURE_TACTICAL => Project::NATURE_TACTICAL,
],
])
->add('description', MarkdownType::class, [
'label' => 'Description du Projet',
'markdown_height' => 900,
]) ])
->add('users', EntityType::class, [ ->add('users', EntityType::class, [

View File

@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\ProjectTimeline;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ProjectTimeline>
*/
class ProjectTimelineRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ProjectTimeline::class);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\ProjectRepository;
use Bnine\FilesBundle\Security\AbstractFileVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class FileVoter extends AbstractFileVoter
{
private ProjectRepository $projectRepository;
public function __construct(ProjectRepository $projectRepository)
{
$this->projectRepository = $projectRepository;
}
protected function canView(string $domain, $id, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return true;
}
protected function canEdit(string $domain, $id, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($user->hasRole('ROLE_ADMIN')) {
return true;
}
switch ($domain) {
case 'project':
$project = $this->projectRepository->find($id);
if ($project && $project->getUsers()->contains($user)) {
return true;
}
break;
}
return false;
}
protected function canDelete(string $domain, $id, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($user->hasRole('ROLE_ADMIN')) {
return true;
}
switch ($domain) {
case 'project':
$project = $this->projectRepository->find($id);
if ($project && $project->getUsers()->contains($user)) {
return true;
}
break;
}
return false;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Security;
use App\Entity\Project;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ProjectVoter extends Voter
{
// Les actions que ce voter supporte
private const EDIT = 'EDIT';
private const VIEW = 'VIEW';
private const DELETE = 'DELETE';
private const MOVEDRAFT = 'MOVEDRAFT';
protected function supports(string $attribute, $subject): bool
{
$attributes = [self::EDIT, self::VIEW, self::DELETE, self::MOVEDRAFT];
return in_array($attribute, $attributes) && $subject instanceof Project;
}
private function canView(Project $project, User $user): bool
{
return true;
}
private function canEdit(Project $project, User $user): bool
{
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
}
private function canDelete(Project $project, User $user): bool
{
return $user->hasRole('ROLE_ADMIN') || (Project::DRAFT === $project->getStatus() && $project->getUsers()->contains($user));
}
private function canMoveDraft(Project $project, User $user): bool
{
return $user->hasRole('ROLE_ADMIN') || (Project::TOVOTE === $project->getStatus() && $project->getUsers()->contains($user));
}
protected function voteOnAttribute(string $attribute, $project, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return match ($attribute) {
self::VIEW => $this->canView($project, $user),
self::EDIT => $this->canEdit($project, $user),
self::DELETE => $this->canDelete($project, $user),
self::MOVEDRAFT => $this->canMoveDraft($project, $user),
default => false,
};
}
}

View File

@ -1,101 +0,0 @@
<?php
namespace App\Service;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelInterface;
class FileService
{
private string $basePath;
private Filesystem $filesystem;
public function __construct(KernelInterface $kernel)
{
$this->filesystem = new Filesystem();
$projectDir = $kernel->getProjectDir(); // chemin racine du projet
$this->basePath = $projectDir.'/uploads';
if (!is_dir($this->basePath)) {
// On crée le dossier uploads s'il n'existe pas
try {
$this->filesystem->mkdir($this->basePath, 0775);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException('Impossible de créer le dossier /uploads : '.$e->getMessage());
}
}
}
/**
* Initialise un répertoire pour une entité (ex: project/123)
*/
public function init(string $domain, string $id): void
{
$entityPath = $this->getEntityPath($domain, $id);
if (!is_dir($entityPath)) {
try {
$this->filesystem->mkdir($entityPath, 0775);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException(sprintf('Impossible de créer le répertoire pour %s/%s : %s', $domain, $id, $e->getMessage()));
}
}
}
/**
* Liste les fichiers dun répertoire lié à une entité (ex: project/123)
*/
public function list(string $domain, string $id, string $relativePath = ''): array
{
$targetPath = $this->getEntityPath($domain, $id).'/'.ltrim($relativePath, '/');
$realPath = realpath($targetPath);
$baseEntityPath = $this->getEntityPath($domain, $id);
if (!$realPath || !str_starts_with($realPath, $baseEntityPath)) {
throw new NotFoundHttpException('Répertoire non autorisé ou inexistant.');
}
$finder = new Finder();
$finder->depth('== 0')->in($realPath);
$results = [];
foreach ($finder as $file) {
$results[] = [
'name' => $file->getFilename(),
'isDirectory' => $file->isDir(),
'path' => ltrim(str_replace($baseEntityPath, '', $file->getRealPath()), '/'),
];
}
return $results;
}
/**
* Supprime un fichier ou dossier (de façon sécurisée)
*/
public function delete(string $domain, string $id, string $relativePath): void
{
$baseEntityPath = $this->getEntityPath($domain, $id);
$targetPath = realpath($baseEntityPath.'/'.ltrim($relativePath, '/'));
if (!$targetPath || !str_starts_with($targetPath, $baseEntityPath)) {
throw new NotFoundHttpException('Fichier ou dossier non autorisé.');
}
try {
$this->filesystem->remove($targetPath);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException('Erreur lors de la suppression : '.$e->getMessage());
}
}
/**
* Construit le chemin absolu dun domaine/id
*/
private function getEntityPath(string $domain, string $id): string
{
return $this->basePath.'/'.$domain.'/'.$id;
}
}

View File

@ -1,4 +1,13 @@
{ {
"bnine/files-bundle": {
"version": "v1.0.1"
},
"bnine/filesbundle": {
"version": "v1.0.4"
},
"bnine/mdeditorbundle": {
"version": "v1.1.1"
},
"doctrine/deprecations": { "doctrine/deprecations": {
"version": "1.1", "version": "1.1",
"recipe": { "recipe": {

View File

@ -20,7 +20,7 @@
<script src="{{ asset('lib/jquery/jquery.min.js') }}"></script> <script src="{{ asset('lib/jquery/jquery.min.js') }}"></script>
<script src="{{ asset('lib/bootstrap/js/bootstrap.bundle.min.js') }}"></script> <script src="{{ asset('lib/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script> <!-- <script src="{{ asset('lib/fontawesome/fontawesome-free.index.js') }}"></script> -->
<script src="{{ asset('lib/datatables/datatables.min.js') }}"></script> <script src="{{ asset('lib/datatables/datatables.min.js') }}"></script>
<script src="{{ asset('lib/datatables/datatables.init.js') }}"></script> <script src="{{ asset('lib/datatables/datatables.init.js') }}"></script>
<script src="{{ asset('lib/select2/select2.min.js') }}"></script> <script src="{{ asset('lib/select2/select2.min.js') }}"></script>
@ -30,6 +30,7 @@
<script src="{{ asset('lib/jqueryui/jquery-ui.min.js') }}"></script> <script src="{{ asset('lib/jqueryui/jquery-ui.min.js') }}"></script>
<script src="{{ asset('lib/chart/chart.js') }}"></script> <script src="{{ asset('lib/chart/chart.js') }}"></script>
<script src="{{ asset('bundles/bninemdeditor/bninemdeditor.js') }}"></script>
<script src="{{ asset('lib/app/app.js') }}"></script> <script src="{{ asset('lib/app/app.js') }}"></script>
{% block javascripts %} {% block javascripts %}

View File

@ -1,124 +0,0 @@
<div id="file-browser-{{ domain }}-{{ id|e('html_attr') }}"
class="file-browser"
data-domain="{{ domain }}"
data-id="{{ id }}"
data-base-path="{{ path('app_files', { domain: domain, id: id, editable: editable }) }}"
data-current-path="{{ path }}">
<div class="card mt-3">
<div class="card-header">Fichiers</div>
<div class="card-body">
{% if editable %}
<div class="mb-3">
<a class="btn btn-info" style="max-width:100%; margin-bottom:15px;" data-bs-toggle="modal" data-bs-target="#mymodal" onClick="ModalLoad('mymodal','Upload','{{ path('app_files_uploadmodal',{domain:domain, id:id,path:path}) }}');" title='Upload'>Upload</a>
</div>
{% endif %}
<p><strong>Chemin :</strong> {{ path ?: '/' }}</p>
{% set parentPath = path|split('/')|slice(0, -1)|join('/') %}
<ul class="list-unstyled">
{% if path %}
<li><a href="#" class="file-nav" data-path="{{ parentPath }}">⬅️ ..</a></li>
{% endif %}
{% for file in files %}
<li>
{% if file.isDirectory %}
📁 <a href="#" class="file-nav" data-path="{{ path ? path ~ '/' ~ file.name : file.name }}">{{ file.name }}/</a>
{% if editable %}
<button class="btn btn-sm btn-danger btn-delete ms-2" data-path="{{ file.path }}">🗑️</button>
{% endif %}
{% else %}
📄 {{ file.name }}
{% if editable %}
<button class="btn btn-sm btn-danger btn-delete ms-2" data-path="{{ file.path }}">🗑️</button>
{% endif %}
{% endif %}
</li>
{% else %}
<li><em>Dossier vide</em></li>
{% endfor %}
</ul>
</div>
</div>
<script>
$(function () {
function refreshContainer(containerId, path) {
const $oldContainer = $('#' + containerId);
const base = $oldContainer.data('base-path');
$.get(base, { path: path }, function (html) {
console.log(html);
const $doc = $('<div>').html(html);
const $newContainer = $doc.find('#' + containerId);
console.log(containerId);
if ($newContainer.length) {
console.log("HHHHHHHHHHHHHHHHHHHH");
$oldContainer.replaceWith($newContainer);
initFileBrowser($newContainer); // rebind events
}
});
}
function initFileBrowser($container) {
const containerId = $container.attr('id');
// Clear any previous bindings (important!)
$container.off('click');
// Navigation dossier
$container.on('click', '.file-nav', function (e) {
e.preventDefault();
const path = $(this).data('path');
refreshContainer(containerId, path);
});
// Suppression fichier ou dossier
$container.on('click', '.btn-delete', function (e) {
e.preventDefault();
if (!confirm('Supprimer ce fichier ?')) return;
const pathToDelete = $(this).data('path');
const currentPath = $container.data('current-path');
$.ajax({
url: '/user/file/' + $container.data('domain') + '/' + $container.data('id') + '/delete',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ path: pathToDelete }),
success: function (res) {
if (res.success) {
refreshContainer(containerId, currentPath);
} else {
alert('Erreur : ' + res.error);
}
},
error: function (xhr) {
alert('Erreur lors de la suppression : ' + xhr.responseText);
}
});
});
}
// Init navigateur fichiers
const containerId = 'file-browser-{{ domain }}-{{ id|e('html_attr') }}';
const $browser = $('#' + containerId);
initFileBrowser($browser);
// Rafraîchir après fermeture modale
$('#mymodal').on('hidden.bs.modal', function () {
const $browser = $('#' + containerId);
const currentPath = $browser.data('current-path') || '';
refreshContainer(containerId, currentPath);
});
});
</script>
</div>

View File

@ -1,45 +0,0 @@
{% extends 'base.html.twig' %}
{% block localstyle %}
<style>
body {
background-color: transparent;
}
</style>
{% endblock %}
{% block body %}
<a class="btn btn-secondary" onClick="closeModal();">Annuler</a>
<form action="{{ path('app_files_uploadfile', {
domain: domain,
id: id,
path: path
}) }}"
class="dropzone" id="myDropzone" style="margin-top:10px"></form>
{% endblock %}
{% block localscript %}
<script>
Dropzone.options.myDropzone = {
paramName: "{{endpoint}}",
maxFilesize: 20, // MB
parallelUploads: 5,
uploadMultiple: false,
dictDefaultMessage: "Déposez vos fichiers ici pour les téléverser",
successmultiple: function (files, response) {
console.log("multi uploaded", files);
},
queuecomplete: function () {
// Quand tous les fichiers sont uploadés, on ferme la modale et rafraîchit le navigateur
window.parent.$("#mymodal").modal('hide');
if (typeof window.parent.refreshFileBrowser === 'function') {
window.parent.refreshFileBrowser(); // à définir côté parent
}
}
};
function closeModal() {
window.parent.$("#mymodal").modal('hide');
}
</script>
{% endblock %}

View File

@ -1,12 +1,25 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{%block body%} {%block body%}
<h2>Projets</h2> <h2>Projets</h2>
<div class='d-flex' style='justify-content: left'>
<div class='d-flex' style='justify-content: center'>
{% for project in projects %} {% for project in projects %}
<div class='card'> <div class='card' style='width:300px'>
<h5>{{project.title}}</h5> <div class='card-header d-flex justify-content-between align-items-center'>
{{ render(path("app_files",{domain:'project',id:project.id, editable:0})) }} {{project.title}}
<div>
{% if is_granted('EDIT', project) %}
<button type="button" class="btn btn-primary btn-sm"><i class="fas fa-pencil"></i></button>
{% endif %}
<button type="button" class="btn btn-secondary btn-sm"><i class="fas fa-eye"></i></button>
</div>
</div>
<div class='card-body'>
</div>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -14,31 +14,40 @@
{% include('include/error.html.twig') %} {% include('include/error.html.twig') %}
<div class="row"> <div class="row">
<div class="col-md-6 mx-auto"> <div class="col-md-4 mx-auto">
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header">Information</div> <div class="card-header">Information</div>
<div class="card-body"> <div class="card-body">
{{ form_row(form.title) }} {{ form_row(form.title) }}
{{ form_row(form.nature) }}
{{ form_row(form.status) }} {{ form_row(form.status) }}
</div>
</div>
</div>
<div class="col-md-6 mx-auto">
<div class="card mt-3">
<div class="card-header">Permissions</div>
<div class="card-body">
{{ form_row(form.users) }} {{ form_row(form.users) }}
</div> {{ form_row(form.summary) }}
</div>
</div> </div>
</div> </div>
{% if mode=="update" %} {{ render(path("bninefiles_files",{domain:'project',id:project.id, editable:1})) }}
{{ render(path("app_files",{domain:'project',id:project.id, editable:1})) }}
{% endif %} <div class="card mt-3">
<div class="card-header">Timeline</div>
<div class="card-body">
{% include('project/timeline.html.twig') %}
</div>
</div>
</div>
<div class="col-md-8 mx-auto">
<div class="card mt-3">
<div class="card-header">Description du Projet</div>
<div class="card-body">
{{ form_widget(form.description) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }} {{ form_end(form) }}
{% endblock %} {% endblock %}
{% block localscript %} {% block localscript %}

View File

@ -22,7 +22,7 @@
<a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a> <a href="{{ path(routeupdate,{id:project.id}) }}" class="me-2"><i class="fas fa-file fa-2x"></i></a>
</td> </td>
<td>{{project.title}}</td> <td>{{project.title}}</td>
<td>{{project.updateAt}}</td> <td>{{project.timelines.first ? project.timelines.first.createdAt|date('d/m/Y H:i') : '' }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -0,0 +1,57 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<div class="timeline">
{% for event in project.timelines %}
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
<p class="text-muted small mb-1">
{{ event.createdAt|date('d/m/Y H:i') }} par <strong>{{ event.user.username }}</strong><br>
{% for field, change in event.description %}
<strong>{{ field }}</strong>
{% if field=="status" %}
<span class="text-danger">{{ change.old ?? '' }}</span>
<span class="text-success">{{ change.new ?? '' }}</span>
{% endif %}
{% if not change.old is defined %}
= <span>{{ change[0] }}</span>
{% endif %}
<br>
{% endfor %}
</p>
</div>
</div>
{% endfor %}
</div>
<style>
.timeline {
border-left: 3px solid #dee2e6;
padding-left: 2rem;
position: relative;
font-size: 14px;
}
.timeline-item {
position: relative;
margin-bottom: 10px;
margin-left: -23px;
}
.timeline-content {
line-height:14px;
}
.timeline-marker {
position: absolute;
left: -1.1rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: #0d6efd;
border: 2px solid #fff;
box-shadow: 0 0 0 3px #0d6efd44;
}
.timeline-content {
padding-left: 1rem;
}
</style>