mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 03:08:25 +00:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			testing-20
			...
			fix-compil
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						88c6e0e0d3
	
				 | 
					
					
						|||
| 
						
						
							
						
						f02c5bca13
	
				 | 
					
					
						|||
| 
						
						
							
						
						0d56828ebd
	
				 | 
					
					
						|||
| 
						
						
							
						
						8b28667fe5
	
				 | 
					
					
						|||
| 
						
						
							
						
						72f73ec8e7
	
				 | 
					
					
						|||
| b3d1320c94 | |||
| 
						
						
							
						
						2ed42e1a2c
	
				 | 
					
					
						|||
| d0e5ba16fe | |||
| 
						
						
							
						
						8e65ad9476
	
				 | 
					
					
						|||
| 
						
						
							
						
						cf7338b690
	
				 | 
					
					
						|||
| 
						
						
							
						
						63dd71037a
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc281762b3
	
				 | 
					
					
						|||
| 
						
						
							
						
						aa0cadfa84
	
				 | 
					
					
						|||
| 
						
						
							
						
						6e2cce9531
	
				 | 
					
					
						|||
| 
						
						
							
						
						1fbbf2b2ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						e586b8ee5e
	
				 | 
					
					
						|||
| 
						
						
							
						
						6d04e477f8
	
				 | 
					
					
						|||
| 
						
						
							
						
						6b7b2ae522
	
				 | 
					
					
						|||
| 
						
						
							
						
						9b9c2774ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						e902b6d409
	
				 | 
					
					
						|||
| 
						
						
							
						
						d8bf6a195f
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c3152f277
	
				 | 
					
					
						|||
| 
						
						
							
						
						cef218fed5
	
				 | 
					
					
						|||
| 
						
						
							
						
						930a76cc66
	
				 | 
					
					
						|||
| 
						
						
							
						
						f11f7498d7
	
				 | 
					
					
						|||
| 
						
						
							
						
						1a9af6b0b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						d347f6ae60
	
				 | 
					
					
						|||
| 
						
						
							
						
						3bb911b4d0
	
				 | 
					
					
						|||
| 
						
						
							
						
						f00b39980c
	
				 | 
					
					
						|||
| 
						
						
							
						
						09882bb4be
	
				 | 
					
					
						|||
| 
						
						
							
						
						1d21499eab
	
				 | 
					
					
						|||
| 8ef001e67e | |||
| 458df45fa5 | |||
| 2b968b9a5b | 
@@ -1,6 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Create new filter for persons having a participation in an accompanying period
 | 
			
		||||
  during a certain time span
 | 
			
		||||
time: 2023-12-18T15:31:51.489901829+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "231"
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: '[Export][List of accompanyign period] Add two columns: the list of persons
 | 
			
		||||
  participating to the period, and their ids'
 | 
			
		||||
time: 2024-01-22T12:48:49.824833412+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "241"
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: 'Add capability to generate export about change of steps of accompanying period, and generate exports for this'
 | 
			
		||||
time: 2024-01-29T13:33:19.190365565+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "244"
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: 'Export: group accompanying period by person participating'
 | 
			
		||||
time: 2024-02-07T10:39:51.97331052+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "253"
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: 'Export: add filter for courses not linked to a reference address'
 | 
			
		||||
time: 2024-02-07T11:46:29.491027007+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "243"
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
kind: Feature
 | 
			
		||||
body: Allow to group activities linked with accompanying period by reason
 | 
			
		||||
time: 2024-02-07T16:40:38.408575109+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "229"
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: Fix error in logs about wrong typing of eventArgs in onEditNotificationComment
 | 
			
		||||
  method
 | 
			
		||||
time: 2023-11-29T11:31:38.933538592+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "220"
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
kind: Fixed
 | 
			
		||||
body: Fix the conditions upon which social actions should be optional or required
 | 
			
		||||
  in relation to social issues within the activity creation form
 | 
			
		||||
time: 2024-01-30T14:03:01.942955636+01:00
 | 
			
		||||
custom:
 | 
			
		||||
  Issue: "256"
 | 
			
		||||
							
								
								
									
										15
									
								
								.changes/v2.16.0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.changes/v2.16.0.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
## v2.16.0 - 2024-02-08
 | 
			
		||||
### Feature
 | 
			
		||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span 
 | 
			
		||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids 
 | 
			
		||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this 
 | 
			
		||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating 
 | 
			
		||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address 
 | 
			
		||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason 
 | 
			
		||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work 
 | 
			
		||||
* Modernize the event bundle, with some new fields and multiple improvements 
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method 
 | 
			
		||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form 
 | 
			
		||||
### UX
 | 
			
		||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin. 
 | 
			
		||||
							
								
								
									
										3
									
								
								.changes/v2.16.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v2.16.1.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
## v2.16.1 - 2024-02-09
 | 
			
		||||
### Fixed
 | 
			
		||||
* Force bootstrap version to avoid error in builds with newer version 
 | 
			
		||||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -6,6 +6,26 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 | 
			
		||||
and is generated by [Changie](https://github.com/miniscruff/changie).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v2.16.1 - 2024-02-09
 | 
			
		||||
### Fixed
 | 
			
		||||
* Force bootstrap version to avoid error in builds with newer version 
 | 
			
		||||
 | 
			
		||||
## v2.16.0 - 2024-02-08
 | 
			
		||||
### Feature
 | 
			
		||||
* ([#231](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/231)) Create new filter for persons having a participation in an accompanying period during a certain time span 
 | 
			
		||||
* ([#241](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/241)) [Export][List of accompanyign period] Add two columns: the list of persons participating to the period, and their ids 
 | 
			
		||||
* ([#244](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/244)) Add capability to generate export about change of steps of accompanying period, and generate exports for this 
 | 
			
		||||
* ([#253](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/253)) Export: group accompanying period by person participating 
 | 
			
		||||
* ([#243](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/243)) Export: add filter for courses not linked to a reference address 
 | 
			
		||||
* ([#229](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/229)) Allow to group activities linked with accompanying period by reason 
 | 
			
		||||
* ([#115](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/115)) Prevent social work to be saved when another user edited conccurently the social work 
 | 
			
		||||
* Modernize the event bundle, with some new fields and multiple improvements 
 | 
			
		||||
### Fixed
 | 
			
		||||
* ([#220](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/220)) Fix error in logs about wrong typing of eventArgs in onEditNotificationComment method 
 | 
			
		||||
* ([#256](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/256)) Fix the conditions upon which social actions should be optional or required in relation to social issues within the activity creation form 
 | 
			
		||||
### UX
 | 
			
		||||
* ([#260](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/260)) Order list of centers alphabetically in dropdown 'user' section admin. 
 | 
			
		||||
 | 
			
		||||
## v2.15.2 - 2024-01-11
 | 
			
		||||
### Fixed
 | 
			
		||||
* Fix the id_seq used when creating a new accompanying period participation during fusion of two person files 
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@
 | 
			
		||||
    ],
 | 
			
		||||
    "require": {
 | 
			
		||||
        "php": "^8.2",
 | 
			
		||||
        "ext-dom": "*",
 | 
			
		||||
        "ext-json": "*",
 | 
			
		||||
        "ext-openssl": "*",
 | 
			
		||||
        "ext-redis": "*",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@
 | 
			
		||||
    "@ckeditor/ckeditor5-vue": "^4.0.1",
 | 
			
		||||
    "@symfony/webpack-encore": "^4.1.0",
 | 
			
		||||
    "@tsconfig/node14": "^1.0.1",
 | 
			
		||||
    "@types/dompurify": "^3.0.5",
 | 
			
		||||
    "bindings": "^1.5.0",
 | 
			
		||||
    "bootstrap": "5.2.3",
 | 
			
		||||
    "chokidar": "^3.5.1",
 | 
			
		||||
    "fork-awesome": "^1.1.7",
 | 
			
		||||
    "jquery": "^3.6.0",
 | 
			
		||||
@@ -34,7 +34,6 @@
 | 
			
		||||
    "webpack-cli": "^5.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "bootstrap": "~5.2.0",
 | 
			
		||||
    "@fullcalendar/core": "^6.1.4",
 | 
			
		||||
    "@fullcalendar/daygrid": "^6.1.4",
 | 
			
		||||
    "@fullcalendar/interaction": "^6.1.4",
 | 
			
		||||
@@ -43,11 +42,9 @@
 | 
			
		||||
    "@fullcalendar/vue3": "^6.1.4",
 | 
			
		||||
    "@popperjs/core": "^2.9.2",
 | 
			
		||||
    "@types/leaflet": "^1.9.3",
 | 
			
		||||
    "dompurify": "^3.0.6",
 | 
			
		||||
    "dropzone": "^5.7.6",
 | 
			
		||||
    "es6-promise": "^4.2.8",
 | 
			
		||||
    "leaflet": "^1.7.1",
 | 
			
		||||
    "marked": "^9.1.5",
 | 
			
		||||
    "masonry-layout": "^4.2.2",
 | 
			
		||||
    "mime": "^3.0.0",
 | 
			
		||||
    "swagger-ui": "^4.15.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class DavTokenAuthenticationEventSubscriberTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    public function testOnJWTAuthenticatedWithDavDataInPayload(): void
 | 
			
		||||
    {
 | 
			
		||||
        $eventSubscriber = new DavTokenAuthenticationEventSubscriber();
 | 
			
		||||
        $token = new class () extends AbstractToken {
 | 
			
		||||
            public function getCredentials()
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        $event = new JWTAuthenticatedEvent([
 | 
			
		||||
            'dav' => 1,
 | 
			
		||||
            'so' => '1234',
 | 
			
		||||
            'e' => 1,
 | 
			
		||||
        ], $token);
 | 
			
		||||
 | 
			
		||||
        $eventSubscriber->onJWTAuthenticated($event);
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
 | 
			
		||||
        self::assertTrue($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
 | 
			
		||||
        self::assertEquals('1234', $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
 | 
			
		||||
        self::assertEquals(StoredObjectRoleEnum::EDIT, $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testOnJWTAuthenticatedWithDavNoDataInPayload(): void
 | 
			
		||||
    {
 | 
			
		||||
        $eventSubscriber = new DavTokenAuthenticationEventSubscriber();
 | 
			
		||||
        $token = new class () extends AbstractToken {
 | 
			
		||||
            public function getCredentials()
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        $event = new JWTAuthenticatedEvent([], $token);
 | 
			
		||||
 | 
			
		||||
        $eventSubscriber->onJWTAuthenticated($event);
 | 
			
		||||
 | 
			
		||||
        self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT));
 | 
			
		||||
        self::assertFalse($token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,252 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
 | 
			
		||||
use Chill\DocStoreBundle\Dav\Response\DavResponse;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide endpoint for editing a document on the desktop using dav.
 | 
			
		||||
 *
 | 
			
		||||
 * This controller implements the minimal required methods to edit a document on a desktop software (i.e. LibreOffice)
 | 
			
		||||
 * and save the document online.
 | 
			
		||||
 *
 | 
			
		||||
 * To avoid to ask for a password, the endpoints are protected using a JWT access token, which is inside the
 | 
			
		||||
 * URL. This avoid the DAV Client (LibreOffice) to keep an access token in query parameter or in some header (which
 | 
			
		||||
 * they are not able to understand). The JWT Guard is adapted with a dedicated token extractor which is going to read
 | 
			
		||||
 * the segments (separation of "/"): the first segment must be the string "dav", and the second one must be the JWT.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class WebdavController
 | 
			
		||||
{
 | 
			
		||||
    private PropfindRequestAnalyzer $requestAnalyzer;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private \Twig\Environment $engine,
 | 
			
		||||
        private StoredObjectManagerInterface $storedObjectManager,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->requestAnalyzer = new PropfindRequestAnalyzer();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
 | 
			
		||||
     */
 | 
			
		||||
    public function getDirectory(StoredObject $storedObject, string $access_token): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new DavResponse(
 | 
			
		||||
            $this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
 | 
			
		||||
                'stored_object' => $storedObject,
 | 
			
		||||
                'access_token' => $access_token,
 | 
			
		||||
            ])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"})
 | 
			
		||||
     */
 | 
			
		||||
    public function optionsDirectory(StoredObject $storedObject): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $response = (new DavResponse(''))
 | 
			
		||||
            ->setEtag($this->storedObjectManager->etag($storedObject))
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        // $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT,PROPPATCH,COPY,MOVE,REPORT,PATCH,POST,TRACE']);
 | 
			
		||||
        $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"})
 | 
			
		||||
     */
 | 
			
		||||
    public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $depth = $request->headers->get('depth');
 | 
			
		||||
 | 
			
		||||
        if ('0' !== $depth && '1' !== $depth) {
 | 
			
		||||
            throw new BadRequestHttpException('only 1 and 0 are accepted for Depth header');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
 | 
			
		||||
 | 
			
		||||
        $response = new DavResponse(
 | 
			
		||||
            $this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
 | 
			
		||||
                'stored_object' => $storedObject,
 | 
			
		||||
                'properties' => $properties,
 | 
			
		||||
                'last_modified' => $lastModified,
 | 
			
		||||
                'etag' => $etag,
 | 
			
		||||
                'content_length' => $length,
 | 
			
		||||
                'depth' => (int) $depth,
 | 
			
		||||
                'access_token' => $access_token,
 | 
			
		||||
            ]),
 | 
			
		||||
            207
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $response->headers->add([
 | 
			
		||||
            'Content-Type' => 'text/xml',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
 | 
			
		||||
     */
 | 
			
		||||
    public function getDocument(StoredObject $storedObject): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (new DavResponse($this->storedObjectManager->read($storedObject)))
 | 
			
		||||
            ->setEtag($this->storedObjectManager->etag($storedObject));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"})
 | 
			
		||||
     */
 | 
			
		||||
    public function headDocument(StoredObject $storedObject): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $response = new DavResponse('');
 | 
			
		||||
 | 
			
		||||
        $response->headers->add(
 | 
			
		||||
            [
 | 
			
		||||
                'Content-Length' => $this->storedObjectManager->getContentLength($storedObject),
 | 
			
		||||
                'Content-Type' => $storedObject->getType(),
 | 
			
		||||
                'Etag' => $this->storedObjectManager->etag($storedObject),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"})
 | 
			
		||||
     */
 | 
			
		||||
    public function optionsDocument(StoredObject $storedObject): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $response = (new DavResponse(''))
 | 
			
		||||
            ->setEtag($this->storedObjectManager->etag($storedObject))
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $response->headers->add(['Allow' =>  'OPTIONS,GET,HEAD,DELETE,PROPFIND,PUT']);
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"})
 | 
			
		||||
     */
 | 
			
		||||
    public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [$properties, $lastModified, $etag, $length] = $this->parseDavRequest($request->getContent(), $storedObject);
 | 
			
		||||
 | 
			
		||||
        $response = new DavResponse(
 | 
			
		||||
            $this->engine->render(
 | 
			
		||||
                '@ChillDocStore/Webdav/doc_props.xml.twig',
 | 
			
		||||
                [
 | 
			
		||||
                    'stored_object' => $storedObject,
 | 
			
		||||
                    'properties' => $properties,
 | 
			
		||||
                    'etag' => $etag,
 | 
			
		||||
                    'last_modified' => $lastModified,
 | 
			
		||||
                    'content_length' => $length,
 | 
			
		||||
                    'access_token' => $access_token,
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
            207
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $response
 | 
			
		||||
            ->headers->add([
 | 
			
		||||
                'Content-Type' => 'text/xml',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"})
 | 
			
		||||
     */
 | 
			
		||||
    public function putDocument(StoredObject $storedObject, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObject)) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storedObjectManager->write($storedObject, $request->getContent());
 | 
			
		||||
 | 
			
		||||
        return new DavResponse('', Response::HTTP_NO_CONTENT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array{0: array, 1: \DateTimeInterface, 2: string, 3: int} properties, lastModified, etag, length
 | 
			
		||||
     */
 | 
			
		||||
    private function parseDavRequest(string $content, StoredObject $storedObject): array
 | 
			
		||||
    {
 | 
			
		||||
        $xml = new \DOMDocument();
 | 
			
		||||
        $xml->loadXML($content);
 | 
			
		||||
 | 
			
		||||
        $properties = $this->requestAnalyzer->getRequestedProperties($xml);
 | 
			
		||||
        $requested = array_keys(array_filter($properties, fn ($item) => true === $item));
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            in_array('lastModified', $requested, true)
 | 
			
		||||
            || in_array('etag', $requested, true)
 | 
			
		||||
        ) {
 | 
			
		||||
            $lastModified = $this->storedObjectManager->getLastModified($storedObject);
 | 
			
		||||
            $etag = $this->storedObjectManager->etag($storedObject);
 | 
			
		||||
        }
 | 
			
		||||
        if (in_array('contentLength', $requested, true)) {
 | 
			
		||||
            $length = $this->storedObjectManager->getContentLength($storedObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            $properties,
 | 
			
		||||
            $lastModified ?? null,
 | 
			
		||||
            $etag ?? null,
 | 
			
		||||
            $length ?? null,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Dav\Exception;
 | 
			
		||||
 | 
			
		||||
class ParseRequestException extends \UnexpectedValueException
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Dav\Request;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Dav\Exception\ParseRequestException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @phpstan-type davProperties array{resourceType: bool, contentType: bool, lastModified: bool, creationDate: bool, contentLength: bool, etag: bool, supportedLock: bool, unknowns: list<array{xmlns: string, prop: string}>}
 | 
			
		||||
 */
 | 
			
		||||
class PropfindRequestAnalyzer
 | 
			
		||||
{
 | 
			
		||||
    private const KNOWN_PROPS = [
 | 
			
		||||
        'resourceType',
 | 
			
		||||
        'contentType',
 | 
			
		||||
        'lastModified',
 | 
			
		||||
        'creationDate',
 | 
			
		||||
        'contentLength',
 | 
			
		||||
        'etag',
 | 
			
		||||
        'supportedLock',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return davProperties
 | 
			
		||||
     */
 | 
			
		||||
    public function getRequestedProperties(\DOMDocument $request): array
 | 
			
		||||
    {
 | 
			
		||||
        $propfinds = $request->getElementsByTagNameNS('DAV:', 'propfind');
 | 
			
		||||
 | 
			
		||||
        if (0 === $propfinds->count()) {
 | 
			
		||||
            throw new ParseRequestException('any propfind element found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (1 < $propfinds->count()) {
 | 
			
		||||
            throw new ParseRequestException('too much propfind element found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $propfind = $propfinds->item(0);
 | 
			
		||||
 | 
			
		||||
        if (0 === $propfind->childNodes->count()) {
 | 
			
		||||
            throw new ParseRequestException('no element under propfind');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $unknows = [];
 | 
			
		||||
        $props = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($propfind->childNodes->getIterator() as $prop) {
 | 
			
		||||
            /** @var \DOMNode $prop */
 | 
			
		||||
            if (XML_ELEMENT_NODE !== $prop->nodeType) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ('propname' === $prop->nodeName) {
 | 
			
		||||
                return $this->baseProps(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach ($prop->childNodes->getIterator() as $getProp) {
 | 
			
		||||
                if (XML_ELEMENT_NODE !== $getProp->nodeType) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ('DAV:' !== $getProp->lookupNamespaceURI(null)) {
 | 
			
		||||
                    $unknows[] = ['xmlns' => $getProp->lookupNamespaceURI(null), 'prop' => $getProp->nodeName];
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $props[] = match ($getProp->nodeName) {
 | 
			
		||||
                    'resourcetype' => 'resourceType',
 | 
			
		||||
                    'getcontenttype' => 'contentType',
 | 
			
		||||
                    'getlastmodified' => 'lastModified',
 | 
			
		||||
                    default => '',
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $props = array_filter(array_values($props), fn (string $item) => '' !== $item);
 | 
			
		||||
 | 
			
		||||
        return [...$this->baseProps(false), ...array_combine($props, array_fill(0, count($props), true)), 'unknowns' => $unknows];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return davProperties
 | 
			
		||||
     */
 | 
			
		||||
    private function baseProps(bool $default = false): array
 | 
			
		||||
    {
 | 
			
		||||
        return
 | 
			
		||||
            [
 | 
			
		||||
                ...array_combine(
 | 
			
		||||
                    self::KNOWN_PROPS,
 | 
			
		||||
                    array_fill(0, count(self::KNOWN_PROPS), $default)
 | 
			
		||||
                ),
 | 
			
		||||
                'unknowns' => [],
 | 
			
		||||
            ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Dav\Response;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
 | 
			
		||||
class DavResponse extends Response
 | 
			
		||||
{
 | 
			
		||||
    public function __construct($content = '', int $status = 200, array $headers = [])
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($content, $status, $headers);
 | 
			
		||||
 | 
			
		||||
        $this->headers->add(['DAV' => '1']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,22 +17,18 @@ window.addEventListener('DOMContentLoaded', function (e) {
 | 
			
		||||
           canEdit: string,
 | 
			
		||||
           storedObject: string,
 | 
			
		||||
           buttonSmall: string,
 | 
			
		||||
           davLink: string,
 | 
			
		||||
           davLinkExpiration: string,
 | 
			
		||||
         };
 | 
			
		||||
 | 
			
		||||
         const
 | 
			
		||||
           storedObject = JSON.parse(datasets.storedObject) as StoredObject,
 | 
			
		||||
           filename = datasets.filename,
 | 
			
		||||
           canEdit = datasets.canEdit === '1',
 | 
			
		||||
           small = datasets.buttonSmall === '1',
 | 
			
		||||
           davLink = 'davLink' in datasets && datasets.davLink !== '' ? datasets.davLink : null,
 | 
			
		||||
           davLinkExpiration = 'davLinkExpiration' in datasets  ? Number.parseInt(datasets.davLinkExpiration) : null
 | 
			
		||||
           small = datasets.buttonSmall === '1'
 | 
			
		||||
           ;
 | 
			
		||||
 | 
			
		||||
         return { storedObject, filename, canEdit, small, davLink, davLinkExpiration };
 | 
			
		||||
         return { storedObject, filename, canEdit, small };
 | 
			
		||||
       },
 | 
			
		||||
       template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" :dav-link="davLink" :dav-link-expiration="davLinkExpiration" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
 | 
			
		||||
       template: '<document-action-buttons-group :can-edit="canEdit" :filename="filename" :stored-object="storedObject" :small="small" @on-stored-object-status-change="onStoredObjectStatusChange"></document-action-buttons-group>',
 | 
			
		||||
       methods: {
 | 
			
		||||
         onStoredObjectStatusChange: function(newStatus: StoredObjectStatusChange): void {
 | 
			
		||||
           this.$data.storedObject.status = newStatus.status;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,6 @@
 | 
			
		||||
      <li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
 | 
			
		||||
        <wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
 | 
			
		||||
        <desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
 | 
			
		||||
        <convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
 | 
			
		||||
      </li>
 | 
			
		||||
@@ -39,7 +36,6 @@ import {
 | 
			
		||||
  StoredObjectStatusChange,
 | 
			
		||||
  WopiEditButtonExecutableBeforeLeaveFunction
 | 
			
		||||
} from "../types";
 | 
			
		||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
 | 
			
		||||
 | 
			
		||||
interface DocumentActionButtonsGroupConfig {
 | 
			
		||||
  storedObject: StoredObject,
 | 
			
		||||
@@ -61,16 +57,6 @@ interface DocumentActionButtonsGroupConfig {
 | 
			
		||||
   * If set, will execute this function before leaving to the editor
 | 
			
		||||
   */
 | 
			
		||||
  executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * a link to download and edit file using webdav
 | 
			
		||||
   */
 | 
			
		||||
  davLink?: string,
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * the expiration date of the download, as a unix timestamp
 | 
			
		||||
   */
 | 
			
		||||
  davLinkExpiration?: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
@@ -82,7 +68,7 @@ const props = withDefaults(defineProps<DocumentActionButtonsGroupConfig>(), {
 | 
			
		||||
  canEdit: true,
 | 
			
		||||
  canDownload: true,
 | 
			
		||||
  canConvertPdf: true,
 | 
			
		||||
  returnPath: window.location.pathname + window.location.search + window.location.hash
 | 
			
		||||
  returnPath: window.location.pathname + window.location.search + window.location.hash,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import {computed, reactive} from "vue";
 | 
			
		||||
 | 
			
		||||
export interface DesktopEditButtonConfig {
 | 
			
		||||
    editLink: null,
 | 
			
		||||
    classes: { [k: string]: boolean },
 | 
			
		||||
    expirationLink: number|Date,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DesktopEditButtonState {
 | 
			
		||||
    modalOpened: boolean
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const state: DesktopEditButtonState = reactive({modalOpened: false});
 | 
			
		||||
 | 
			
		||||
const props = defineProps<DesktopEditButtonConfig>();
 | 
			
		||||
 | 
			
		||||
const buildCommand = computed<string>(() => 'vnd.libreoffice.command:ofe|u|' + props.editLink);
 | 
			
		||||
 | 
			
		||||
const editionUntilFormatted = computed<string>(() => {
 | 
			
		||||
    let d;
 | 
			
		||||
 | 
			
		||||
    if (props.expirationLink instanceof Date) {
 | 
			
		||||
        d = props.expirationLink;
 | 
			
		||||
    } else {
 | 
			
		||||
        d = new Date(props.expirationLink * 1000);
 | 
			
		||||
    }
 | 
			
		||||
    console.log(props.expirationLink);
 | 
			
		||||
 | 
			
		||||
    return (new Intl.DateTimeFormat(undefined, {'dateStyle': 'long', 'timeStyle': 'medium'})).format(d);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <teleport to="body">
 | 
			
		||||
        <modal v-if="state.modalOpened" @close="state.modalOpened=false">
 | 
			
		||||
            <template v-slot:body>
 | 
			
		||||
                <div class="desktop-edit">
 | 
			
		||||
                    <p class="center">Veuillez enregistrer vos modifications avant le</p>
 | 
			
		||||
                    <p><strong>{{ editionUntilFormatted }}</strong></p>
 | 
			
		||||
 | 
			
		||||
                    <p><a class="btn btn-primary" :href="buildCommand">Ouvrir le document pour édition</a></p>
 | 
			
		||||
 | 
			
		||||
                    <p><small>Le document peut être édité uniquement en utilisant Libre Office.</small></p>
 | 
			
		||||
 | 
			
		||||
                    <p><small>En cas d'échec lors de l'enregistrement, sauver le document sur le poste de travail avant de le déposer à nouveau ici.</small></p>
 | 
			
		||||
 | 
			
		||||
                    <p><small>Vous pouvez naviguez sur d'autres pages pendant l'édition.</small></p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </modal>
 | 
			
		||||
    </teleport>
 | 
			
		||||
    <a :class="props.classes" @click="state.modalOpened = true">
 | 
			
		||||
        <i class="fa fa-desktop"></i>
 | 
			
		||||
        Éditer sur le bureau
 | 
			
		||||
    </a>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.desktop-edit {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -3,7 +3,5 @@
 | 
			
		||||
    data-download-buttons
 | 
			
		||||
    data-stored-object="{{ document_json|json_encode|escape('html_attr') }}"
 | 
			
		||||
    data-can-edit="{{ can_edit ? '1' : '0' }}"
 | 
			
		||||
    data-dav-link="{{ dav_link|escape('html_attr') }}"
 | 
			
		||||
    data-dav-link-expiration="{{ dav_link_expiration|escape('html_attr') }}"
 | 
			
		||||
    {% if options['small'] is defined %}data-button-small="{{ options['small'] ? '1' : '0' }}"{% endif %}
 | 
			
		||||
    {% if title|default(document.title)|default(null) is not null %}data-filename="{{ title|default(document.title)|escape('html_attr') }}"{% endif %}></div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <title>Directory for {{ stored_object.uuid }}</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<ul>
 | 
			
		||||
    <li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">d</a></li>
 | 
			
		||||
</ul>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" ?>
 | 
			
		||||
<d:multistatus xmlns:d="DAV:">
 | 
			
		||||
    <d:response>
 | 
			
		||||
        <d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid, 'access_token': access_token } ) }}</d:href>
 | 
			
		||||
        {% if properties.resourceType or properties.contentType %}
 | 
			
		||||
            <d:propstat>
 | 
			
		||||
                <d:prop>
 | 
			
		||||
                    {% if properties.resourceType %}
 | 
			
		||||
                        <d:resourcetype><d:collection/></d:resourcetype>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.contentType %}
 | 
			
		||||
                        <d:getcontenttype>httpd/unix-directory</d:getcontenttype>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </d:prop>
 | 
			
		||||
                <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
            </d:propstat>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if properties.unknowns|length > 0 %}
 | 
			
		||||
            <d:propstat>
 | 
			
		||||
                {% for k,u in properties.unknowns %}
 | 
			
		||||
                    <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
 | 
			
		||||
                        <{{ 'ns'~ k ~ ':' ~ u.prop }} />
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
            </d:propstat>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </d:response>
 | 
			
		||||
    {% if depth == 1 %}
 | 
			
		||||
        <d:response>
 | 
			
		||||
            <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token':access_token}) }}</d:href>
 | 
			
		||||
            {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
 | 
			
		||||
                <d:propstat>
 | 
			
		||||
                    <d:prop>
 | 
			
		||||
                        {% if properties.resourceType %}
 | 
			
		||||
                            <d:resourcetype/>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if properties.creationDate %}
 | 
			
		||||
                            <d:creationdate />
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if properties.lastModified %}
 | 
			
		||||
                            {%  if last_modified is not same as null %}
 | 
			
		||||
                                <d:getlastmodified>{{  last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <d:getlastmodified />
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if properties.contentLength %}
 | 
			
		||||
                            {%  if content_length is not same as null %}
 | 
			
		||||
                                <d:getcontentlength>{{ content_length }}</d:getcontentlength>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <d:getcontentlength />
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if properties.etag %}
 | 
			
		||||
                            {% if etag is not same as null %}
 | 
			
		||||
                                <d:getetag>"{{ etag }}"</d:getetag>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                                <d:getetag />
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if properties.contentType %}
 | 
			
		||||
                            <d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                    <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
                </d:propstat>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if properties.unknowns|length > 0 %}
 | 
			
		||||
                <d:propstat>
 | 
			
		||||
                    {% for k,u in properties.unknowns %}
 | 
			
		||||
                        <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
 | 
			
		||||
                            <{{ 'ns'~ k ~ ':' ~ u.prop }} />
 | 
			
		||||
                        </d:prop>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                    <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
                </d:propstat>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </d:response>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</d:multistatus>
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" ?>
 | 
			
		||||
<d:multistatus xmlns:d="DAV:">
 | 
			
		||||
    <d:response>
 | 
			
		||||
        <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token}) }}</d:href>
 | 
			
		||||
        {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %}
 | 
			
		||||
            <d:propstat>
 | 
			
		||||
                <d:prop>
 | 
			
		||||
                    {% if properties.resourceType %}
 | 
			
		||||
                        <d:resourcetype/>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.creationDate %}
 | 
			
		||||
                        <d:creationdate />
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.lastModified %}
 | 
			
		||||
                        {%  if last_modified is not same as null %}
 | 
			
		||||
                            <d:getlastmodified>{{  last_modified.format(constant('DATE_RSS')) }}</d:getlastmodified>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <d:getlastmodified />
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.contentLength %}
 | 
			
		||||
                        {%  if content_length is not same as null %}
 | 
			
		||||
                        <d:getcontentlength>{{ content_length }}</d:getcontentlength>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <d:getcontentlength />
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.etag %}
 | 
			
		||||
                        {% if etag is not same as null %}
 | 
			
		||||
                            <d:getetag>"{{ etag }}"</d:getetag>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                            <d:getetag />
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if properties.contentType %}
 | 
			
		||||
                        <d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </d:prop>
 | 
			
		||||
                <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
            </d:propstat>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        {% if properties.unknowns|length > 0 %}
 | 
			
		||||
            <d:propstat>
 | 
			
		||||
                {% for k,u in properties.unknowns %}
 | 
			
		||||
                    <d:prop {{ ('xmlns:ns' ~ k ~ '="' ~ u.xmlns|e('html_attr') ~ '"')|raw }}>
 | 
			
		||||
                        <{{ 'ns'~ k ~ ':' ~ u.prop }} />
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
                <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
            </d:propstat>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </d:response>
 | 
			
		||||
</d:multistatus>
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <p>document uuid: {{ stored_object.uuid }}</p>
 | 
			
		||||
    <p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token  })) }}</p>
 | 
			
		||||
<a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">Open document</a>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Role to edit or see the stored object content.
 | 
			
		||||
 */
 | 
			
		||||
enum StoredObjectRoleEnum: string
 | 
			
		||||
{
 | 
			
		||||
    case SEE = 'SEE';
 | 
			
		||||
 | 
			
		||||
    case EDIT = 'SEE_AND_EDIT';
 | 
			
		||||
}
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Voter for the content of a stored object.
 | 
			
		||||
 *
 | 
			
		||||
 * This is in use to allow or disallow the edition of the stored object's content.
 | 
			
		||||
 */
 | 
			
		||||
class StoredObjectVoter extends Voter
 | 
			
		||||
{
 | 
			
		||||
    protected function supports($attribute, $subject): bool
 | 
			
		||||
    {
 | 
			
		||||
        return StoredObjectRoleEnum::tryFrom($attribute) instanceof StoredObjectRoleEnum
 | 
			
		||||
            && $subject instanceof StoredObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var StoredObject $subject */
 | 
			
		||||
        if (
 | 
			
		||||
            !$token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
 | 
			
		||||
            || $subject->getUuid()->toString() !== $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)
 | 
			
		||||
        ) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $askedRole = StoredObjectRoleEnum::from($attribute);
 | 
			
		||||
        $tokenRoleAuthorization =
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS);
 | 
			
		||||
 | 
			
		||||
        return match ($askedRole) {
 | 
			
		||||
            StoredObjectRoleEnum::SEE => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization || StoredObjectRoleEnum::SEE === $tokenRoleAuthorization,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT => StoredObjectRoleEnum::EDIT === $tokenRoleAuthorization
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extract the JWT Token from the segment of the dav endpoints.
 | 
			
		||||
 *
 | 
			
		||||
 * A segment is a separation inside the string, using the character "/".
 | 
			
		||||
 *
 | 
			
		||||
 * For recognizing the JWT, the first segment must be "dav", and the second one must be
 | 
			
		||||
 * the JWT endpoint.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class DavOnUrlTokenExtractor implements TokenExtractorInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private LoggerInterface $logger,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function extract(Request $request): false|string
 | 
			
		||||
    {
 | 
			
		||||
        $uri = $request->getRequestUri();
 | 
			
		||||
 | 
			
		||||
        $segments = array_values(
 | 
			
		||||
            array_filter(
 | 
			
		||||
                explode('/', $uri),
 | 
			
		||||
                fn ($item) => '' !== trim($item)
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (2 > count($segments)) {
 | 
			
		||||
            $this->logger->info('not enough segment for parsing URL');
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('dav' !== $segments[0]) {
 | 
			
		||||
            $this->logger->info('the first segment of the url must be DAV');
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $segments[1];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Events;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Store some data from the JWT's payload inside the token's attributes.
 | 
			
		||||
 */
 | 
			
		||||
class DavTokenAuthenticationEventSubscriber implements EventSubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    final public const STORED_OBJECT = 'stored_object';
 | 
			
		||||
    final public const ACTIONS = 'stored_objects_actions';
 | 
			
		||||
 | 
			
		||||
    public static function getSubscribedEvents(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            Events::JWT_AUTHENTICATED => ['onJWTAuthenticated', 0],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function onJWTAuthenticated(JWTAuthenticatedEvent $event): void
 | 
			
		||||
    {
 | 
			
		||||
        $payload = $event->getPayload();
 | 
			
		||||
 | 
			
		||||
        if (!(array_key_exists('dav', $payload) && 1 === $payload['dav'])) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $token = $event->getToken();
 | 
			
		||||
        $token->setAttribute(self::ACTIONS, match ($payload['e']) {
 | 
			
		||||
            0 => StoredObjectRoleEnum::SEE,
 | 
			
		||||
            1 => StoredObjectRoleEnum::EDIT,
 | 
			
		||||
            default => throw new \UnexpectedValueException('unsupported value for e parameter')
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $token->setAttribute(self::STORED_OBJECT, $payload['so']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide a JWT Token which will be valid for viewing or editing a document.
 | 
			
		||||
 */
 | 
			
		||||
final readonly class JWTDavTokenProvider implements JWTDavTokenProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private JWTTokenManagerInterface $JWTTokenManager,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createToken(StoredObject $storedObject, StoredObjectRoleEnum $roleEnum): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->JWTTokenManager->createFromPayload($this->security->getUser(), [
 | 
			
		||||
            'dav' => 1,
 | 
			
		||||
            'e' => match ($roleEnum) {
 | 
			
		||||
                StoredObjectRoleEnum::SEE => 0,
 | 
			
		||||
                StoredObjectRoleEnum::EDIT => 1,
 | 
			
		||||
            },
 | 
			
		||||
            'so' => $storedObject->getUuid(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTokenExpiration(string $tokenString): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        $jwt = $this->JWTTokenManager->parse($tokenString);
 | 
			
		||||
 | 
			
		||||
        return \DateTimeImmutable::createFromFormat('U', (string) $jwt['exp']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide a JWT Token which will be valid for viewing or editing a document.
 | 
			
		||||
 */
 | 
			
		||||
interface JWTDavTokenProviderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function createToken(StoredObject $storedObject, StoredObjectRoleEnum $roleEnum): string;
 | 
			
		||||
 | 
			
		||||
    public function getTokenExpiration(string $tokenString): \DateTimeImmutable;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
 | 
			
		||||
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 | 
			
		||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Alter the base JWTTokenAuthenticator to add the special extractor for dav url endpoints.
 | 
			
		||||
 */
 | 
			
		||||
class JWTOnDavUrlAuthenticator extends JWTTokenAuthenticator
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        JWTTokenManagerInterface $jwtManager,
 | 
			
		||||
        EventDispatcherInterface $dispatcher,
 | 
			
		||||
        TokenExtractorInterface $tokenExtractor,
 | 
			
		||||
        private readonly DavOnUrlTokenExtractor $davOnUrlTokenExtractor,
 | 
			
		||||
        TokenStorageInterface $preAuthenticationTokenStorage,
 | 
			
		||||
        TranslatorInterface $translator = null,
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $preAuthenticationTokenStorage, $translator);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getTokenExtractor()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->davOnUrlTokenExtractor;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -57,62 +57,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
 | 
			
		||||
        return $this->extractLastModifiedFromResponse($response);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContentLength(StoredObject $document): int
 | 
			
		||||
    {
 | 
			
		||||
        if ([] === $document->getKeyInfos()) {
 | 
			
		||||
            if ($this->hasCache($document)) {
 | 
			
		||||
                $response = $this->getResponseFromCache($document);
 | 
			
		||||
            } else {
 | 
			
		||||
                try {
 | 
			
		||||
                    $response = $this
 | 
			
		||||
                        ->client
 | 
			
		||||
                        ->request(
 | 
			
		||||
                            Request::METHOD_HEAD,
 | 
			
		||||
                            $this
 | 
			
		||||
                                ->tempUrlGenerator
 | 
			
		||||
                                ->generate(
 | 
			
		||||
                                    Request::METHOD_HEAD,
 | 
			
		||||
                                    $document->getFilename()
 | 
			
		||||
                                )
 | 
			
		||||
                                ->url
 | 
			
		||||
                        );
 | 
			
		||||
                } catch (TransportExceptionInterface $exception) {
 | 
			
		||||
                    throw StoredObjectManagerException::errorDuringHttpRequest($exception);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $this->extractContentLengthFromResponse($response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return strlen($this->read($document));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function etag(StoredObject $document): string
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->hasCache($document)) {
 | 
			
		||||
            $response = $this->getResponseFromCache($document);
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                $response = $this
 | 
			
		||||
                    ->client
 | 
			
		||||
                    ->request(
 | 
			
		||||
                        Request::METHOD_HEAD,
 | 
			
		||||
                        $this
 | 
			
		||||
                            ->tempUrlGenerator
 | 
			
		||||
                            ->generate(
 | 
			
		||||
                                Request::METHOD_HEAD,
 | 
			
		||||
                                $document->getFilename()
 | 
			
		||||
                            )
 | 
			
		||||
                            ->url
 | 
			
		||||
                    );
 | 
			
		||||
            } catch (TransportExceptionInterface $exception) {
 | 
			
		||||
                throw StoredObjectManagerException::errorDuringHttpRequest($exception);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->extractEtagFromResponse($response, $document);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function read(StoredObject $document): string
 | 
			
		||||
    {
 | 
			
		||||
        $response = $this->getResponseFromCache($document);
 | 
			
		||||
@@ -202,22 +146,6 @@ final class StoredObjectManager implements StoredObjectManagerInterface
 | 
			
		||||
        return $date;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function extractContentLengthFromResponse(ResponseInterface $response): int
 | 
			
		||||
    {
 | 
			
		||||
        return (int) ($response->getHeaders()['content-length'] ?? ['0'])[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function extractEtagFromResponse(ResponseInterface $response, StoredObject $storedObject): ?string
 | 
			
		||||
    {
 | 
			
		||||
        $etag = ($response->getHeaders()['etag'] ?? [''])[0];
 | 
			
		||||
 | 
			
		||||
        if ('' === $etag) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $etag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function fillCache(StoredObject $document): void
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,6 @@ interface StoredObjectManagerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function getLastModified(StoredObject $document): \DateTimeInterface;
 | 
			
		||||
 | 
			
		||||
    public function getContentLength(StoredObject $document): int;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the content of a StoredObject.
 | 
			
		||||
     *
 | 
			
		||||
@@ -35,6 +33,4 @@ interface StoredObjectManagerInterface
 | 
			
		||||
     * @param              $clearContent The content to store in clear
 | 
			
		||||
     */
 | 
			
		||||
    public function write(StoredObject $document, string $clearContent): void;
 | 
			
		||||
 | 
			
		||||
    public function etag(StoredObject $document): string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,6 @@ namespace Chill\DocStoreBundle\Templating;
 | 
			
		||||
 | 
			
		||||
use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
@@ -123,12 +120,8 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
 | 
			
		||||
 | 
			
		||||
    private const TEMPLATE_BUTTON_GROUP = '@ChillDocStore/Button/button_group.html.twig';
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private DiscoveryInterface $discovery,
 | 
			
		||||
        private NormalizerInterface $normalizer,
 | 
			
		||||
        private JWTDavTokenProviderInterface $davTokenProvider,
 | 
			
		||||
        private UrlGeneratorInterface $urlGenerator,
 | 
			
		||||
    ) {
 | 
			
		||||
    public function __construct(private DiscoveryInterface $discovery, private NormalizerInterface $normalizer)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -139,7 +132,7 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
 | 
			
		||||
     */
 | 
			
		||||
    public function isEditable(StoredObject $document): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($document->getType(), self::SUPPORTED_MIMES, true);
 | 
			
		||||
        return \in_array($document->getType(), self::SUPPORTED_MIMES, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -151,26 +144,12 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
 | 
			
		||||
     */
 | 
			
		||||
    public function renderButtonGroup(Environment $environment, StoredObject $document, ?string $title = null, bool $canEdit = true, array $options = []): string
 | 
			
		||||
    {
 | 
			
		||||
        $accessToken = $this->davTokenProvider->createToken(
 | 
			
		||||
            $document,
 | 
			
		||||
            $canEdit ? StoredObjectRoleEnum::EDIT : StoredObjectRoleEnum::SEE
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $environment->render(self::TEMPLATE_BUTTON_GROUP, [
 | 
			
		||||
            'document' => $document,
 | 
			
		||||
            'document_json' => $this->normalizer->normalize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
            'title' => $title,
 | 
			
		||||
            'can_edit' => $canEdit,
 | 
			
		||||
            'options' => [...self::DEFAULT_OPTIONS_TEMPLATE_BUTTON_GROUP, ...$options],
 | 
			
		||||
            'dav_link' => $this->urlGenerator->generate(
 | 
			
		||||
                'chill_docstore_dav_document_get',
 | 
			
		||||
                [
 | 
			
		||||
                    'uuid' => $document->getUuid(),
 | 
			
		||||
                    'access_token' => $accessToken,
 | 
			
		||||
                ],
 | 
			
		||||
                UrlGeneratorInterface::ABSOLUTE_URL,
 | 
			
		||||
            ),
 | 
			
		||||
            'dav_link_expiration' => $this->davTokenProvider->getTokenExpiration($accessToken)->format('U'),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,410 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Controller\WebdavController;
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Ramsey\Uuid\Uuid;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class WebdavControllerTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    private \Twig\Environment $engine;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
 | 
			
		||||
        $this->engine = self::$container->get(\Twig\Environment::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildController(): WebdavController
 | 
			
		||||
    {
 | 
			
		||||
        $storedObjectManager = new MockedStoredObjectManager();
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted(Argument::in(['EDIT', 'SEE']), Argument::type(StoredObject::class))
 | 
			
		||||
            ->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        return new WebdavController($this->engine, $storedObjectManager, $security->reveal());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildDocument(): StoredObject
 | 
			
		||||
    {
 | 
			
		||||
        $object = (new StoredObject())
 | 
			
		||||
            ->setType('application/vnd.oasis.opendocument.text');
 | 
			
		||||
 | 
			
		||||
        $reflectionObject = new \ReflectionClass($object);
 | 
			
		||||
        $reflectionObjectUuid = $reflectionObject->getProperty('uuid');
 | 
			
		||||
 | 
			
		||||
        $reflectionObjectUuid->setValue($object, Uuid::fromString('716e6688-4579-4938-acf3-c4ab5856803b'));
 | 
			
		||||
 | 
			
		||||
        return $object;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testGet(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
 | 
			
		||||
        $response = $controller->getDocument($this->buildDocument());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(200, $response->getStatusCode());
 | 
			
		||||
        self::assertEquals('abcde', $response->getContent());
 | 
			
		||||
        self::assertContains('etag', $response->headers->keys());
 | 
			
		||||
        self::assertStringContainsString('ab56b4', $response->headers->get('etag'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testOptionsOnDocument(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
 | 
			
		||||
        $response = $controller->optionsDocument($this->buildDocument());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(200, $response->getStatusCode());
 | 
			
		||||
        self::assertContains('allow', $response->headers->keys());
 | 
			
		||||
 | 
			
		||||
        foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
 | 
			
		||||
            self::assertStringContainsString($method, $response->headers->get('allow'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self::assertContains('dav', $response->headers->keys());
 | 
			
		||||
        self::assertStringContainsString('1', $response->headers->get('dav'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testOptionsOnDirectory(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
 | 
			
		||||
        $response = $controller->optionsDirectory($this->buildDocument());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(200, $response->getStatusCode());
 | 
			
		||||
        self::assertContains('allow', $response->headers->keys());
 | 
			
		||||
 | 
			
		||||
        foreach (explode(',', 'OPTIONS,GET,HEAD,PROPFIND') as $method) {
 | 
			
		||||
            self::assertStringContainsString($method, $response->headers->get('allow'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self::assertContains('dav', $response->headers->keys());
 | 
			
		||||
        self::assertStringContainsString('1', $response->headers->get('dav'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateDataPropfindDocument
 | 
			
		||||
     */
 | 
			
		||||
    public function testPropfindDocument(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
 | 
			
		||||
        $request = new Request([], [], [], [], [], [], $requestContent);
 | 
			
		||||
        $request->setMethod('PROPFIND');
 | 
			
		||||
        $response = $controller->propfindDocument($this->buildDocument(), '1234', $request);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expectedStatusCode, $response->getStatusCode());
 | 
			
		||||
        self::assertContains('content-type', $response->headers->keys());
 | 
			
		||||
        self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
 | 
			
		||||
        self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
 | 
			
		||||
        self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateDataPropfindDirectory
 | 
			
		||||
     */
 | 
			
		||||
    public function testPropfindDirectory(string $requestContent, int $expectedStatusCode, string $expectedXmlResponse, string $message): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
 | 
			
		||||
        $request = new Request([], [], [], [], [], [], $requestContent);
 | 
			
		||||
        $request->setMethod('PROPFIND');
 | 
			
		||||
        $request->headers->add(['Depth' => '0']);
 | 
			
		||||
        $response = $controller->propfindDirectory($this->buildDocument(), '1234', $request);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expectedStatusCode, $response->getStatusCode());
 | 
			
		||||
        self::assertContains('content-type', $response->headers->keys());
 | 
			
		||||
        self::assertStringContainsString('text/xml', $response->headers->get('content-type'));
 | 
			
		||||
        self::assertTrue((new \DOMDocument())->loadXML($response->getContent()), $message.' test that the xml response is a valid xml');
 | 
			
		||||
        self::assertXmlStringEqualsXmlString($expectedXmlResponse, $response->getContent(), $message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHeadDocument(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController();
 | 
			
		||||
        $response = $controller->headDocument($this->buildDocument());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(200, $response->getStatusCode());
 | 
			
		||||
        self::assertContains('content-length', $response->headers->keys());
 | 
			
		||||
        self::assertContains('content-type', $response->headers->keys());
 | 
			
		||||
        self::assertContains('etag', $response->headers->keys());
 | 
			
		||||
        self::assertEquals('ab56b4d92b40713acc5af89985d4b786', $response->headers->get('etag'));
 | 
			
		||||
        self::assertEquals('application/vnd.oasis.opendocument.text', $response->headers->get('content-type'));
 | 
			
		||||
        self::assertEquals(5, $response->headers->get('content-length'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function generateDataPropfindDocument(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $content =
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
            <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
 | 
			
		||||
            XML;
 | 
			
		||||
 | 
			
		||||
        $response =
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
            <d:multistatus xmlns:d="DAV:" >
 | 
			
		||||
            <d:response>
 | 
			
		||||
              <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
 | 
			
		||||
              <d:propstat>
 | 
			
		||||
                <d:prop>
 | 
			
		||||
                  <d:resourcetype/>
 | 
			
		||||
                  <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
 | 
			
		||||
                </d:prop>
 | 
			
		||||
                <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
              </d:propstat>
 | 
			
		||||
              <d:propstat>
 | 
			
		||||
                <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
 | 
			
		||||
                  <ns0:IsReadOnly/>
 | 
			
		||||
                </d:prop>
 | 
			
		||||
                <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
              </d:propstat>
 | 
			
		||||
            </d:response>
 | 
			
		||||
            </d:multistatus>
 | 
			
		||||
            XML;
 | 
			
		||||
 | 
			
		||||
        yield [$content, 207, $response, 'get IsReadOnly and contenttype from server'];
 | 
			
		||||
 | 
			
		||||
        $content =
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <prop>
 | 
			
		||||
                  <IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/>
 | 
			
		||||
                </prop>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML;
 | 
			
		||||
 | 
			
		||||
        $response =
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
              <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
 | 
			
		||||
                  <d:propstat>
 | 
			
		||||
                    <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
 | 
			
		||||
                      <ns0:IsReadOnly/>
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                    <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
                  </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
              </d:multistatus>
 | 
			
		||||
            XML;
 | 
			
		||||
 | 
			
		||||
        yield [$content, 207, $response, 'get property IsReadOnly'];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <prop>
 | 
			
		||||
                  <BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/>
 | 
			
		||||
                </prop>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            207,
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
              <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
 | 
			
		||||
                  <d:propstat>
 | 
			
		||||
                    <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
 | 
			
		||||
                      <ns0:BaseURI/>
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                    <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
                  </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
              </d:multistatus>
 | 
			
		||||
            XML,
 | 
			
		||||
            'Test requesting an unknow property',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <prop>
 | 
			
		||||
                  <getlastmodified xmlns="DAV:"/>
 | 
			
		||||
                </prop>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            207,
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
              <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
 | 
			
		||||
                  <d:propstat>
 | 
			
		||||
                    <d:prop>
 | 
			
		||||
                    <!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT -->
 | 
			
		||||
                      <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                    <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
                  </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
              </d:multistatus>
 | 
			
		||||
            XML,
 | 
			
		||||
            'test getting the last modified date',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <propname/>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            207,
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
              <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                  <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href>
 | 
			
		||||
                  <d:propstat>
 | 
			
		||||
                    <d:prop>
 | 
			
		||||
                      <d:resourcetype/>
 | 
			
		||||
                      <d:creationdate/>
 | 
			
		||||
                      <d:getlastmodified>Wed, 13 Sep 2023 14:15:00 +0200</d:getlastmodified>
 | 
			
		||||
                      <!-- <d:getcontentlength/> -->
 | 
			
		||||
                      <d:getcontentlength>5</d:getcontentlength>
 | 
			
		||||
                      <!-- <d:getlastmodified/> -->
 | 
			
		||||
                      <d:getetag>"ab56b4d92b40713acc5af89985d4b786"</d:getetag>
 | 
			
		||||
                      <!--
 | 
			
		||||
                      <d:supportedlock/>
 | 
			
		||||
                      <d:lockdiscovery/>
 | 
			
		||||
                      -->
 | 
			
		||||
                     <!-- <d:getcontenttype/> -->
 | 
			
		||||
                      <d:getcontenttype>application/vnd.oasis.opendocument.text</d:getcontenttype>
 | 
			
		||||
                    </d:prop>
 | 
			
		||||
                    <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
                  </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
              </d:multistatus>
 | 
			
		||||
            XML,
 | 
			
		||||
            'test finding all properties',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function generateDataPropfindDirectory(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
            <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            207,
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
            <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                    <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
 | 
			
		||||
                    <d:propstat>
 | 
			
		||||
                        <d:prop>
 | 
			
		||||
                            <d:resourcetype><d:collection/></d:resourcetype>
 | 
			
		||||
                            <d:getcontenttype>httpd/unix-directory</d:getcontenttype>
 | 
			
		||||
                            <!--
 | 
			
		||||
                            <d:supportedlock>
 | 
			
		||||
                                <d:lockentry>
 | 
			
		||||
                                    <d:lockscope><d:exclusive/></d:lockscope>
 | 
			
		||||
                                    <d:locktype><d:write/></d:locktype>
 | 
			
		||||
                                </d:lockentry>
 | 
			
		||||
                                <d:lockentry>
 | 
			
		||||
                                    <d:lockscope><d:shared/></d:lockscope>
 | 
			
		||||
                                    <d:locktype><d:write/></d:locktype>
 | 
			
		||||
                                </d:lockentry>
 | 
			
		||||
                            </d:supportedlock>
 | 
			
		||||
                            -->
 | 
			
		||||
                        </d:prop>
 | 
			
		||||
                        <d:status>HTTP/1.1 200 OK</d:status>
 | 
			
		||||
                    </d:propstat>
 | 
			
		||||
                    <d:propstat>
 | 
			
		||||
                        <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/">
 | 
			
		||||
                            <ns0:IsReadOnly/>
 | 
			
		||||
                        </d:prop>
 | 
			
		||||
                        <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
                    </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
            </d:multistatus>
 | 
			
		||||
            XML,
 | 
			
		||||
            'test resourceType and IsReadOnly ',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
            <propfind xmlns="DAV:"><prop><CreatableContentsInfo xmlns="http://ucb.openoffice.org/dav/props/"/></prop></propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            207,
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
            <d:multistatus xmlns:d="DAV:">
 | 
			
		||||
                <d:response>
 | 
			
		||||
                    <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href>
 | 
			
		||||
                    <d:propstat>
 | 
			
		||||
                        <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" >
 | 
			
		||||
                            <ns0:CreatableContentsInfo/>
 | 
			
		||||
                        </d:prop>
 | 
			
		||||
                        <d:status>HTTP/1.1 404 Not Found</d:status>
 | 
			
		||||
                    </d:propstat>
 | 
			
		||||
                </d:response>
 | 
			
		||||
            </d:multistatus>
 | 
			
		||||
            XML,
 | 
			
		||||
            'test creatableContentsInfo',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MockedStoredObjectManager implements StoredObjectManagerInterface
 | 
			
		||||
{
 | 
			
		||||
    public function getLastModified(StoredObject $document): \DateTimeInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new \DateTimeImmutable('2023-09-13T14:15');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContentLength(StoredObject $document): int
 | 
			
		||||
    {
 | 
			
		||||
        return 5;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function read(StoredObject $document): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'abcde';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function write(StoredObject $document, string $clearContent): void
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function etag(StoredObject $document): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'ab56b4d92b40713acc5af89985d4b786';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,134 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Dav\Request;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class PropfindRequestAnalyzerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideRequestedProperties
 | 
			
		||||
     */
 | 
			
		||||
    public function testGetRequestedProperties(string $xml, array $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $analyzer = new PropfindRequestAnalyzer();
 | 
			
		||||
 | 
			
		||||
        $request = new \DOMDocument();
 | 
			
		||||
        $request->loadXML($xml);
 | 
			
		||||
        $actual = $analyzer->getRequestedProperties($request);
 | 
			
		||||
 | 
			
		||||
        foreach ($expected as $key => $value) {
 | 
			
		||||
            if ('unknowns' === $key) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self::assertArrayHasKey($key, $actual, "Check that key {$key} does exists in list of expected values");
 | 
			
		||||
            self::assertEquals($value, $actual[$key], "Does the value match expected for key {$key}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (array_key_exists('unknowns', $expected)) {
 | 
			
		||||
            self::assertEquals(count($expected['unknowns']), count($actual['unknowns']));
 | 
			
		||||
            self::assertEqualsCanonicalizing($expected['unknowns'], $actual['unknowns']);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function provideRequestedProperties(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
            <propfind xmlns="DAV:">
 | 
			
		||||
                <prop>
 | 
			
		||||
                    <BaseURI xmlns="http://ucb.openoffice.org/dav/props/"/>
 | 
			
		||||
                </prop>
 | 
			
		||||
            </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            [
 | 
			
		||||
                'resourceType' => false,
 | 
			
		||||
                'contentType' => false,
 | 
			
		||||
                'lastModified' => false,
 | 
			
		||||
                'creationDate' => false,
 | 
			
		||||
                'contentLength' => false,
 | 
			
		||||
                'etag' => false,
 | 
			
		||||
                'supportedLock' => false,
 | 
			
		||||
                'unknowns' => [
 | 
			
		||||
                    ['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'BaseURI'],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <propname/>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            [
 | 
			
		||||
                'resourceType' => true,
 | 
			
		||||
                'contentType' => true,
 | 
			
		||||
                'lastModified' => true,
 | 
			
		||||
                'creationDate' => true,
 | 
			
		||||
                'contentLength' => true,
 | 
			
		||||
                'etag' => true,
 | 
			
		||||
                'supportedLock' => true,
 | 
			
		||||
                'unknowns' => [],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
              <propfind xmlns="DAV:">
 | 
			
		||||
                <prop>
 | 
			
		||||
                  <getlastmodified xmlns="DAV:"/>
 | 
			
		||||
                </prop>
 | 
			
		||||
              </propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            [
 | 
			
		||||
                'resourceType' => false,
 | 
			
		||||
                'contentType' => false,
 | 
			
		||||
                'lastModified' => true,
 | 
			
		||||
                'creationDate' => false,
 | 
			
		||||
                'contentLength' => false,
 | 
			
		||||
                'etag' => false,
 | 
			
		||||
                'supportedLock' => false,
 | 
			
		||||
                'unknowns' => [],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            <<<'XML'
 | 
			
		||||
            <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
            <propfind xmlns="DAV:"><prop><resourcetype xmlns="DAV:"/><IsReadOnly xmlns="http://ucb.openoffice.org/dav/props/"/><getcontenttype xmlns="DAV:"/><supportedlock xmlns="DAV:"/></prop></propfind>
 | 
			
		||||
            XML,
 | 
			
		||||
            [
 | 
			
		||||
                'resourceType' => true,
 | 
			
		||||
                'contentType' => true,
 | 
			
		||||
                'lastModified' => false,
 | 
			
		||||
                'creationDate' => false,
 | 
			
		||||
                'contentLength' => false,
 | 
			
		||||
                'etag' => false,
 | 
			
		||||
                'supportedLock' => false,
 | 
			
		||||
                'unknowns' => [
 | 
			
		||||
                    ['xmlns' => 'http://ucb.openoffice.org/dav/props/', 'prop' => 'IsReadOnly'],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Security\Authorization;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter;
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavTokenAuthenticationEventSubscriber;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class StoredObjectVoterTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideDataVote
 | 
			
		||||
     */
 | 
			
		||||
    public function testVote(TokenInterface $token, null|object $subject, string $attribute, mixed $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $voter = new StoredObjectVoter();
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $voter->vote($token, $subject, [$attribute]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function provideDataVote(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, new StoredObject()),
 | 
			
		||||
            new \stdClass(),
 | 
			
		||||
            'SOMETHING',
 | 
			
		||||
            VoterInterface::ACCESS_ABSTAIN,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            'SOMETHING',
 | 
			
		||||
            VoterInterface::ACCESS_ABSTAIN,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::EDIT, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::EDIT->value,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(StoredObjectRoleEnum::SEE, $so = new StoredObject()),
 | 
			
		||||
            $so,
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_GRANTED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(null, null),
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            $this->buildToken(null, null),
 | 
			
		||||
            new StoredObject(),
 | 
			
		||||
            StoredObjectRoleEnum::SEE->value,
 | 
			
		||||
            VoterInterface::ACCESS_DENIED,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildToken(StoredObjectRoleEnum $storedObjectRoleEnum = null, StoredObject $storedObject = null): TokenInterface
 | 
			
		||||
    {
 | 
			
		||||
        $token = $this->prophesize(TokenInterface::class);
 | 
			
		||||
 | 
			
		||||
        if (null !== $storedObjectRoleEnum) {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(true);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn($storedObjectRoleEnum);
 | 
			
		||||
        } else {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willReturn(false);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::ACTIONS)->willThrow(new \InvalidArgumentException());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $storedObject) {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(true);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn($storedObject->getUuid()->toString());
 | 
			
		||||
        } else {
 | 
			
		||||
            $token->hasAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willReturn(false);
 | 
			
		||||
            $token->getAttribute(DavTokenAuthenticationEventSubscriber::STORED_OBJECT)->willThrow(new \InvalidArgumentException());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $token->reveal();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\DocStoreBundle\Tests\Security\Guard;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Security\Guard\DavOnUrlTokenExtractor;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class DavOnUrlTokenExtractorTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideDataUri
 | 
			
		||||
     */
 | 
			
		||||
    public function testExtract(string $uri, false|string $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $request = $this->prophesize(Request::class);
 | 
			
		||||
        $request->getRequestUri()->willReturn($uri);
 | 
			
		||||
 | 
			
		||||
        $extractor = new DavOnUrlTokenExtractor(new NullLogger());
 | 
			
		||||
 | 
			
		||||
        $actual = $extractor->extract($request->reveal());
 | 
			
		||||
 | 
			
		||||
        self::assertEquals($expected, $actual);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @phpstan-pure
 | 
			
		||||
     */
 | 
			
		||||
    public static function provideDataUri(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield ['/dav/123456789/get/d07d2230-5326-11ee-8fd4-93696acf5ea1/d', '123456789'];
 | 
			
		||||
        yield ['/dav/123456789', '123456789'];
 | 
			
		||||
        yield ['/not-dav/123456978', false];
 | 
			
		||||
        yield ['/dav', false];
 | 
			
		||||
        yield ['/', false];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -34,11 +34,6 @@ services:
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
        autowire: true
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\Security\:
 | 
			
		||||
        resource: './../Security'
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
        autowire: true
 | 
			
		||||
 | 
			
		||||
    Chill\DocStoreBundle\Serializer\Normalizer\:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        resource: '../Serializer/Normalizer/'
 | 
			
		||||
 
 | 
			
		||||
@@ -17,10 +17,11 @@ use Chill\EventBundle\Form\EventType;
 | 
			
		||||
use Chill\EventBundle\Form\Type\PickEventType;
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\EventVoter;
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Form\Type\PickPersonType;
 | 
			
		||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
 | 
			
		||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
 | 
			
		||||
@@ -37,53 +38,26 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
 | 
			
		||||
use Symfony\Component\Form\FormFactoryInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\StreamedResponse;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class EventController.
 | 
			
		||||
 */
 | 
			
		||||
class EventController extends AbstractController
 | 
			
		||||
final class EventController extends AbstractController
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var AuthorizationHelper
 | 
			
		||||
     */
 | 
			
		||||
    protected $authorizationHelper;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var EventDispatcherInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $eventDispatcher;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var FormFactoryInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $formFactoryInterface;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var PaginatorFactory
 | 
			
		||||
     */
 | 
			
		||||
    protected $paginator;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var TranslatorInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $translator;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * EventController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EventDispatcherInterface $eventDispatcher,
 | 
			
		||||
        AuthorizationHelper $authorizationHelper,
 | 
			
		||||
        FormFactoryInterface $formFactoryInterface,
 | 
			
		||||
        TranslatorInterface $translator,
 | 
			
		||||
        PaginatorFactory $paginator
 | 
			
		||||
        private readonly EventDispatcherInterface $eventDispatcher,
 | 
			
		||||
        private readonly AuthorizationHelperInterface $authorizationHelper,
 | 
			
		||||
        private readonly FormFactoryInterface $formFactoryInterface,
 | 
			
		||||
        private readonly TranslatorInterface $translator,
 | 
			
		||||
        private readonly PaginatorFactory $paginator,
 | 
			
		||||
        private readonly Security $security,
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->eventDispatcher = $eventDispatcher;
 | 
			
		||||
        $this->authorizationHelper = $authorizationHelper;
 | 
			
		||||
        $this->formFactoryInterface = $formFactoryInterface;
 | 
			
		||||
        $this->translator = $translator;
 | 
			
		||||
        $this->paginator = $paginator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -181,7 +155,7 @@ class EventController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person);
 | 
			
		||||
 | 
			
		||||
        $reachablesCircles = $this->authorizationHelper->getReachableCircles(
 | 
			
		||||
        $reachablesCircles = $this->authorizationHelper->getReachableScopes(
 | 
			
		||||
            $this->getUser(),
 | 
			
		||||
            EventVoter::SEE,
 | 
			
		||||
            $person->getCenter()
 | 
			
		||||
@@ -233,6 +207,12 @@ class EventController extends AbstractController
 | 
			
		||||
     */
 | 
			
		||||
    public function newAction(?Center $center, Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $user = $this->security->getUser();
 | 
			
		||||
 | 
			
		||||
        if (!$user instanceof User) {
 | 
			
		||||
            throw new AccessDeniedHttpException('not a regular user. Maybe an administrator ?');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null === $center) {
 | 
			
		||||
            $center_id = $request->query->get('center_id');
 | 
			
		||||
            $center = $this->getDoctrine()->getRepository(Center::class)->find($center_id);
 | 
			
		||||
@@ -240,6 +220,7 @@ class EventController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        $entity = new Event();
 | 
			
		||||
        $entity->setCenter($center);
 | 
			
		||||
        $entity->setLocation($user->getCurrentLocation());
 | 
			
		||||
 | 
			
		||||
        $form = $this->createCreateForm($entity);
 | 
			
		||||
        $form->handleRequest($request);
 | 
			
		||||
@@ -282,7 +263,7 @@ class EventController extends AbstractController
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $form = $this->formFactoryInterface
 | 
			
		||||
            ->createNamedBuilder(null, FormType::class, null, [
 | 
			
		||||
            ->createNamedBuilder('', FormType::class, null, [
 | 
			
		||||
                'csrf_protection' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->setMethod('GET')
 | 
			
		||||
@@ -323,7 +304,7 @@ class EventController extends AbstractController
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->denyAccessUnlessGranted(
 | 
			
		||||
            'CHILL_EVENT_SEE_DETAILS',
 | 
			
		||||
            EventVoter::SEE_DETAILS,
 | 
			
		||||
            $event,
 | 
			
		||||
            'You are not allowed to see details on this event'
 | 
			
		||||
        );
 | 
			
		||||
@@ -367,7 +348,7 @@ class EventController extends AbstractController
 | 
			
		||||
            $this->addFlash('success', $this->translator
 | 
			
		||||
                ->trans('The event was updated'));
 | 
			
		||||
 | 
			
		||||
            return $this->redirectToRoute('chill_event__event_edit', ['event_id' => $event_id]);
 | 
			
		||||
            return $this->redirectToRoute('chill_event__event_show', ['event_id' => $event_id]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->render('@ChillEvent/Event/edit.html.twig', [
 | 
			
		||||
@@ -385,7 +366,7 @@ class EventController extends AbstractController
 | 
			
		||||
    {
 | 
			
		||||
        /** @var \Symfony\Component\Form\FormBuilderInterface $builder */
 | 
			
		||||
        $builder = $this
 | 
			
		||||
            ->get('form.factory')
 | 
			
		||||
            ->formFactoryInterface
 | 
			
		||||
            ->createNamedBuilder(
 | 
			
		||||
                null,
 | 
			
		||||
                FormType::class,
 | 
			
		||||
@@ -430,11 +411,9 @@ class EventController extends AbstractController
 | 
			
		||||
     */
 | 
			
		||||
    protected function createAddParticipationByPersonForm(Event $event)
 | 
			
		||||
    {
 | 
			
		||||
        /** @var \Symfony\Component\Form\FormBuilderInterface $builder */
 | 
			
		||||
        $builder = $this
 | 
			
		||||
            ->get('form.factory')
 | 
			
		||||
        $builder = $this->formFactoryInterface
 | 
			
		||||
            ->createNamedBuilder(
 | 
			
		||||
                null,
 | 
			
		||||
                '',
 | 
			
		||||
                FormType::class,
 | 
			
		||||
                null,
 | 
			
		||||
                [
 | 
			
		||||
@@ -444,23 +423,17 @@ class EventController extends AbstractController
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        $builder->add('person_id', PickPersonType::class, [
 | 
			
		||||
            'role' => 'CHILL_EVENT_CREATE',
 | 
			
		||||
            'centers' => $event->getCenter(),
 | 
			
		||||
        $builder->add('person_id', PickPersonDynamicType::class, [
 | 
			
		||||
            'as_id' => true,
 | 
			
		||||
            'multiple' => false,
 | 
			
		||||
            'submit_on_adding_new_entity' => true,
 | 
			
		||||
            'label' => 'Add a participation',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $builder->add('event_id', HiddenType::class, [
 | 
			
		||||
            'data' => $event->getId(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $builder->add(
 | 
			
		||||
            'submit',
 | 
			
		||||
            SubmitType::class,
 | 
			
		||||
            [
 | 
			
		||||
                'label' => 'Add a participation',
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $builder->getForm();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -469,7 +442,7 @@ class EventController extends AbstractController
 | 
			
		||||
     */
 | 
			
		||||
    protected function createExportByFormatForm()
 | 
			
		||||
    {
 | 
			
		||||
        $builder = $this->createFormBuilder()
 | 
			
		||||
        $builder = $this->createFormBuilder(['format' => 'xlsx'])
 | 
			
		||||
            ->add('format', ChoiceType::class, [
 | 
			
		||||
                'choices' => [
 | 
			
		||||
                    'xlsx' => 'xlsx',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										118
									
								
								src/Bundle/ChillEventBundle/Controller/EventListController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/Bundle/ChillEventBundle/Controller/EventListController.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Entity\EventType;
 | 
			
		||||
use Chill\EventBundle\Repository\EventACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\EventBundle\Repository\EventTypeRepository;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
 | 
			
		||||
use Symfony\Component\Form\FormFactoryInterface;
 | 
			
		||||
use Symfony\Component\Form\FormInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class EventListController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Environment $environment,
 | 
			
		||||
        private EventACLAwareRepositoryInterface $eventACLAwareRepository,
 | 
			
		||||
        private EventTypeRepository $eventTypeRepository,
 | 
			
		||||
        private FilterOrderHelperFactory $filterOrderHelperFactory,
 | 
			
		||||
        private FormFactoryInterface $formFactory,
 | 
			
		||||
        private PaginatorFactoryInterface $paginatorFactory,
 | 
			
		||||
        private TranslatableStringHelperInterface $translatableStringHelper,
 | 
			
		||||
        private UrlGeneratorInterface $urlGenerator,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("{_locale}/event/event/list", name="chill_event_event_list")
 | 
			
		||||
     */
 | 
			
		||||
    public function __invoke(): Response
 | 
			
		||||
    {
 | 
			
		||||
        $filter = $this->buildFilterOrder();
 | 
			
		||||
        $filterData = [
 | 
			
		||||
            'q' => (string) $filter->getQueryString(),
 | 
			
		||||
            'dates' => $filter->getDateRangeData('dates'),
 | 
			
		||||
            'event_types' => $filter->getEntityChoiceData('event_types'),
 | 
			
		||||
        ];
 | 
			
		||||
        $total = $this->eventACLAwareRepository->countAllViewable($filterData);
 | 
			
		||||
        $pagination = $this->paginatorFactory->create($total);
 | 
			
		||||
        $events = $this->eventACLAwareRepository->findAllViewable($filterData, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage());
 | 
			
		||||
        $eventForms = [];
 | 
			
		||||
        foreach ($events as $event) {
 | 
			
		||||
            $eventForms[$event->getId()] = $this->createAddParticipationByPersonForm($event)->createView();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new Response($this->environment->render(
 | 
			
		||||
            '@ChillEvent/Event/page_list.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
                'events' => $events,
 | 
			
		||||
                'pagination' => $pagination,
 | 
			
		||||
                'eventForms' => $eventForms,
 | 
			
		||||
                'filter' => $filter,
 | 
			
		||||
            ]
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildFilterOrder(): FilterOrderHelper
 | 
			
		||||
    {
 | 
			
		||||
        $types = $this->eventTypeRepository->findAllActive();
 | 
			
		||||
 | 
			
		||||
        $builder = $this->filterOrderHelperFactory->create(__METHOD__);
 | 
			
		||||
        $builder
 | 
			
		||||
            ->addDateRange('dates', 'event.filter.event_dates')
 | 
			
		||||
            ->addSearchBox(['name'])
 | 
			
		||||
            ->addEntityChoice('event_types', 'event.filter.event_types', EventType::class, $types, [
 | 
			
		||||
                'choice_label' => fn (EventType $e) => $this->translatableStringHelper->localize($e->getName()),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        return $builder->build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function createAddParticipationByPersonForm(Event $event): FormInterface
 | 
			
		||||
    {
 | 
			
		||||
        $builder = $this->formFactory
 | 
			
		||||
            ->createNamedBuilder(
 | 
			
		||||
                '',
 | 
			
		||||
                FormType::class,
 | 
			
		||||
                null,
 | 
			
		||||
                [
 | 
			
		||||
                    'method' => 'GET',
 | 
			
		||||
                    'action' => $this->urlGenerator->generate('chill_event_participation_new'),
 | 
			
		||||
                    'csrf_protection' => false,
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        $builder->add('person_id', PickPersonDynamicType::class, [
 | 
			
		||||
            'as_id' => true,
 | 
			
		||||
            'multiple' => false,
 | 
			
		||||
            'submit_on_adding_new_entity' => true,
 | 
			
		||||
            'label' => 'Add a participation',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $builder->add('event_id', HiddenType::class, [
 | 
			
		||||
            'data' => $event->getId(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return $builder->getForm();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,7 +14,10 @@ namespace Chill\EventBundle\Controller;
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Entity\Participation;
 | 
			
		||||
use Chill\EventBundle\Form\ParticipationType;
 | 
			
		||||
use Chill\EventBundle\Repository\EventRepository;
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonRepository;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Psr\Log\LoggerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
@@ -28,13 +31,17 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
/**
 | 
			
		||||
 * Class ParticipationController.
 | 
			
		||||
 */
 | 
			
		||||
class ParticipationController extends AbstractController
 | 
			
		||||
final class ParticipationController extends AbstractController
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * ParticipationController constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(private readonly LoggerInterface $logger, private readonly TranslatorInterface $translator)
 | 
			
		||||
    {
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly LoggerInterface $logger,
 | 
			
		||||
        private readonly TranslatorInterface $translator,
 | 
			
		||||
        private readonly EventRepository $eventRepository,
 | 
			
		||||
        private readonly PersonRepository $personRepository,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -230,6 +237,7 @@ class ParticipationController extends AbstractController
 | 
			
		||||
        return $this->render('@ChillEvent/Participation/new.html.twig', [
 | 
			
		||||
            'form' => $form->createView(),
 | 
			
		||||
            'participation' => $participation,
 | 
			
		||||
            'ignored_participations' => [],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -539,7 +547,7 @@ class ParticipationController extends AbstractController
 | 
			
		||||
     * If the request is multiple, the $participation object is cloned.
 | 
			
		||||
     * Limitations: the $participation should not be persisted.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Participation|Participation[] return one single participation if $multiple == false
 | 
			
		||||
     * @return Participation|list<Participation> return one single participation if $multiple == false
 | 
			
		||||
     *
 | 
			
		||||
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException    if the event/person is not found
 | 
			
		||||
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException if the user does not have access to event/person
 | 
			
		||||
@@ -556,30 +564,25 @@ class ParticipationController extends AbstractController
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $event_id = $request->query->getInt('event_id', 0); // sf4 check:
 | 
			
		||||
        // prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
 | 
			
		||||
 | 
			
		||||
        if (null !== $event_id) {
 | 
			
		||||
            $event = $em->getRepository(Event::class)
 | 
			
		||||
                ->find($event_id);
 | 
			
		||||
        $event = $this->eventRepository->find($event_id);
 | 
			
		||||
 | 
			
		||||
            if (null === $event) {
 | 
			
		||||
                throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->denyAccessUnlessGranted(
 | 
			
		||||
                'CHILL_EVENT_SEE',
 | 
			
		||||
                $event,
 | 
			
		||||
                'The user is not allowed to see the event'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            $participation->setEvent($event);
 | 
			
		||||
        if (null === $event) {
 | 
			
		||||
            throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->denyAccessUnlessGranted(
 | 
			
		||||
            'CHILL_EVENT_SEE',
 | 
			
		||||
            $event,
 | 
			
		||||
            'The user is not allowed to see the event'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $participation->setEvent($event);
 | 
			
		||||
 | 
			
		||||
        // this script should be able to handle multiple, so we translate
 | 
			
		||||
        // single person_id in an array
 | 
			
		||||
        $persons_ids = $request->query->has('person_id') ?
 | 
			
		||||
            [$request->query->getInt('person_id', 0)]  // sf4 check:
 | 
			
		||||
               // prevent error: `Argument 2 passed to ::getInt() must be of the type int, null given`
 | 
			
		||||
            [$request->query->get('person_id', 0)]
 | 
			
		||||
            : explode(',', (string) $request->query->get('persons_ids'));
 | 
			
		||||
        $participations = [];
 | 
			
		||||
 | 
			
		||||
@@ -588,15 +591,14 @@ class ParticipationController extends AbstractController
 | 
			
		||||
            $participation = \count($persons_ids) > 1 ? clone $participation : $participation;
 | 
			
		||||
 | 
			
		||||
            if (null !== $person_id) {
 | 
			
		||||
                $person = $em->getRepository(\Chill\PersonBundle\Entity\Person::class)
 | 
			
		||||
                    ->find($person_id);
 | 
			
		||||
                $person = $this->personRepository->find($person_id);
 | 
			
		||||
 | 
			
		||||
                if (null === $person) {
 | 
			
		||||
                    throw $this->createNotFoundException('The person with id '.$person_id.' is not found');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $this->denyAccessUnlessGranted(
 | 
			
		||||
                    'CHILL_PERSON_SEE',
 | 
			
		||||
                    PersonVoter::SEE,
 | 
			
		||||
                    $person,
 | 
			
		||||
                    'The user is not allowed to see the person'
 | 
			
		||||
                );
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\EventBundle\DependencyInjection;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\EventVoter;
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
 | 
			
		||||
use Symfony\Component\Config\FileLocator;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
 | 
			
		||||
@@ -33,10 +34,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
        $loader->load('services/authorization.yaml');
 | 
			
		||||
        $loader->load('services/controller.yaml');
 | 
			
		||||
        $loader->load('services/fixtures.yaml');
 | 
			
		||||
        $loader->load('services/forms.yaml');
 | 
			
		||||
        $loader->load('services/menu.yaml');
 | 
			
		||||
        $loader->load('services/repositories.yaml');
 | 
			
		||||
        $loader->load('services/search.yaml');
 | 
			
		||||
        $loader->load('services/timeline.yaml');
 | 
			
		||||
@@ -61,6 +60,8 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
 | 
			
		||||
                EventVoter::SEE_DETAILS => [EventVoter::SEE],
 | 
			
		||||
                EventVoter::UPDATE => [EventVoter::SEE_DETAILS],
 | 
			
		||||
                EventVoter::CREATE => [EventVoter::SEE_DETAILS],
 | 
			
		||||
                ParticipationVoter::SEE_DETAILS => [ParticipationVoter::SEE],
 | 
			
		||||
                ParticipationVoter::UPDATE => [ParticipationVoter::SEE_DETAILS],
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,15 +11,23 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
 | 
			
		||||
use Chill\MainBundle\Entity\HasCenterInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\HasScopeInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\Location;
 | 
			
		||||
use Chill\MainBundle\Entity\Scope;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use DateTime;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class Event.
 | 
			
		||||
@@ -30,10 +38,15 @@ use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\HasLifecycleCallbacks
 | 
			
		||||
 */
 | 
			
		||||
class Event implements HasCenterInterface, HasScopeInterface
 | 
			
		||||
class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull()
 | 
			
		||||
     */
 | 
			
		||||
    private ?Center $center = null;
 | 
			
		||||
 | 
			
		||||
@@ -63,6 +76,8 @@ class Event implements HasCenterInterface, HasScopeInterface
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="string", length=150)
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotBlank()
 | 
			
		||||
     */
 | 
			
		||||
    private ?string $name = null;
 | 
			
		||||
 | 
			
		||||
@@ -77,15 +92,45 @@ class Event implements HasCenterInterface, HasScopeInterface
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\EventType")
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull()
 | 
			
		||||
     */
 | 
			
		||||
    private ?EventType $type = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
 | 
			
		||||
     */
 | 
			
		||||
    private CommentEmbeddable $comment;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity=Location::class)
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\JoinColumn(nullable=true)
 | 
			
		||||
     */
 | 
			
		||||
    private ?Location $location = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<StoredObject>
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\ManyToMany(targetEntity=StoredObject::class, cascade={"persist","refresh"})
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\JoinTable("chill_event_event_documents")
 | 
			
		||||
     */
 | 
			
		||||
    private Collection $documents;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="decimal", precision=10, scale=4, nullable=true, options={"default": null})
 | 
			
		||||
     */
 | 
			
		||||
    private string $organizationCost = '0.0';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Event constructor.
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->participations = new ArrayCollection();
 | 
			
		||||
        $this->documents = new ArrayCollection();
 | 
			
		||||
        $this->comment = new CommentEmbeddable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -100,6 +145,22 @@ class Event implements HasCenterInterface, HasScopeInterface
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addDocument(StoredObject $storedObject): self
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->documents->contains($storedObject)) {
 | 
			
		||||
            $this->documents[] = $storedObject;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeDocument(StoredObject $storedObject): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->documents->removeElement($storedObject);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Center
 | 
			
		||||
     */
 | 
			
		||||
@@ -259,4 +320,44 @@ class Event implements HasCenterInterface, HasScopeInterface
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getComment(): CommentEmbeddable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->comment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setComment(CommentEmbeddable $comment): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->comment = $comment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLocation(): ?Location
 | 
			
		||||
    {
 | 
			
		||||
        return $this->location;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLocation(?Location $location): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->location = $location;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDocuments(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->documents;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setDocuments(Collection $documents): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->documents = $documents;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getOrganizationCost(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->organizationCost;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setOrganizationCost(string $organizationCost): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->organizationCost = $organizationCost;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,17 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\HasCenterInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\HasScopeInterface;
 | 
			
		||||
use Chill\MainBundle\Entity\Scope;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use DateTime;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -26,12 +30,20 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
 | 
			
		||||
 * @ORM\Entity(
 | 
			
		||||
 * repositoryClass="Chill\EventBundle\Repository\ParticipationRepository")
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\Table(name="chill_event_participation")
 | 
			
		||||
 * @ORM\Table(name="chill_event_participation", uniqueConstraints={
 | 
			
		||||
 *
 | 
			
		||||
 *     @ORM\UniqueConstraint(name="chill_event_participation_event_person_unique_idx", columns={"event_id", "person_id"})
 | 
			
		||||
 *     })
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\HasLifecycleCallbacks
 | 
			
		||||
 *
 | 
			
		||||
 * @UniqueEntity({"event", "person"}, message="event.validation.person_already_participate_to_event")
 | 
			
		||||
 */
 | 
			
		||||
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface
 | 
			
		||||
class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterface, TrackUpdateInterface, TrackCreationInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(
 | 
			
		||||
     *     targetEntity="Chill\EventBundle\Entity\Event",
 | 
			
		||||
@@ -48,13 +60,10 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     */
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="datetime")
 | 
			
		||||
     */
 | 
			
		||||
    private ?\DateTime $lastUpdate = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull()
 | 
			
		||||
     */
 | 
			
		||||
    private ?Person $person = null;
 | 
			
		||||
 | 
			
		||||
@@ -65,12 +74,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status")
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull()
 | 
			
		||||
     */
 | 
			
		||||
    private ?Status $status = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Center
 | 
			
		||||
     */
 | 
			
		||||
    public function getCenter()
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $this->getEvent()) {
 | 
			
		||||
@@ -90,10 +98,8 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get id.
 | 
			
		||||
     *
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function getId()
 | 
			
		||||
    public function getId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,11 +107,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
    /**
 | 
			
		||||
     * Get lastUpdate.
 | 
			
		||||
     *
 | 
			
		||||
     * @return \DateTime
 | 
			
		||||
     * @return \DateTimeInterface|null
 | 
			
		||||
     */
 | 
			
		||||
    public function getLastUpdate()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->lastUpdate;
 | 
			
		||||
        return $this->getUpdatedAt();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -235,10 +241,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     */
 | 
			
		||||
    public function setEvent(?Event $event = null)
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->event !== $event) {
 | 
			
		||||
            $this->update();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->event = $event;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
@@ -251,10 +253,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     */
 | 
			
		||||
    public function setPerson(?Person $person = null)
 | 
			
		||||
    {
 | 
			
		||||
        if ($person !== $this->person) {
 | 
			
		||||
            $this->update();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->person = $person;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
@@ -267,9 +265,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     */
 | 
			
		||||
    public function setRole(?Role $role = null)
 | 
			
		||||
    {
 | 
			
		||||
        if ($role !== $this->role) {
 | 
			
		||||
            $this->update();
 | 
			
		||||
        }
 | 
			
		||||
        $this->role = $role;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
@@ -282,10 +277,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     */
 | 
			
		||||
    public function setStatus(?Status $status = null)
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->status !== $status) {
 | 
			
		||||
            $this->update();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->status = $status;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
@@ -295,11 +286,11 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
 | 
			
		||||
     * Set lastUpdate.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Participation
 | 
			
		||||
     *
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     */
 | 
			
		||||
    protected function update()
 | 
			
		||||
    {
 | 
			
		||||
        $this->lastUpdate = new \DateTime('now');
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,18 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\DocStoreBundle\Entity\StoredObject;
 | 
			
		||||
use Chill\DocStoreBundle\Form\StoredObjectType;
 | 
			
		||||
use Chill\EventBundle\Form\Type\PickEventTypeType;
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillDateTimeType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\CommentType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\PickUserLocationType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ScopePickerType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\UserPickerType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +53,28 @@ class EventType extends AbstractType
 | 
			
		||||
                    'class' => '',
 | 
			
		||||
                ],
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('location', PickUserLocationType::class, [
 | 
			
		||||
                'label' => 'event.fields.location',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('comment', CommentType::class, [
 | 
			
		||||
                'label' => 'Comment',
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('documents', ChillCollectionType::class, [
 | 
			
		||||
                'entry_type' => StoredObjectType::class,
 | 
			
		||||
                'entry_options' => [
 | 
			
		||||
                    'has_title' => true,
 | 
			
		||||
                ],
 | 
			
		||||
                'allow_add' => true,
 | 
			
		||||
                'allow_delete' => true,
 | 
			
		||||
                'delete_empty' => fn (StoredObject $storedObject): bool => '' === $storedObject->getFilename(),
 | 
			
		||||
                'button_remove_label' => 'event.form.remove_document',
 | 
			
		||||
                'button_add_label' => 'event.form.add_document',
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('organizationCost', MoneyType::class, [
 | 
			
		||||
                'label' => 'event.fields.organizationCost',
 | 
			
		||||
                'help' => 'event.form.organisationCost_help',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,7 @@ final class PickEventType extends AbstractType
 | 
			
		||||
        } else {
 | 
			
		||||
            $centers = $this->authorizationHelper->getReachableCenters(
 | 
			
		||||
                $user,
 | 
			
		||||
                (string) $options['role']->getRole()
 | 
			
		||||
                $options['role']
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Menu;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\EventVoter;
 | 
			
		||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
 | 
			
		||||
use Knp\Menu\MenuItem;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class SectionMenuBuilder implements LocalMenuBuilderInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private TranslatorInterface $translator,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildMenu($menuId, MenuItem $menu, array $parameters)
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->security->isGranted(EventVoter::SEE)) {
 | 
			
		||||
            $menu->addChild(
 | 
			
		||||
                $this->translator->trans('Events'),
 | 
			
		||||
                [
 | 
			
		||||
                    'route' => 'chill_event_event_list',
 | 
			
		||||
                ]
 | 
			
		||||
            )->setExtras([
 | 
			
		||||
                'order' => 250,
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getMenuIds(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['section'];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,142 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Entity\Participation;
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\EventVoter;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\NonUniqueResultException;
 | 
			
		||||
use Doctrine\ORM\NoResultException;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class EventACLAwareRepository implements EventACLAwareRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws NonUniqueResultException
 | 
			
		||||
     * @throws NoResultException
 | 
			
		||||
     */
 | 
			
		||||
    public function countAllViewable(array $filters): int
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->getUser() instanceof User) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb = $this->buildQueryByAllViewable($filters);
 | 
			
		||||
        $this->addFilters($filters, $qb);
 | 
			
		||||
 | 
			
		||||
        $qb->select('COUNT(event.id)');
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->getUser() instanceof User) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb = $this->buildQueryByAllViewable($filters)->select('event');
 | 
			
		||||
        $this->addFilters($filters, $qb);
 | 
			
		||||
 | 
			
		||||
        $qb->setFirstResult($offset)->setMaxResults($limit);
 | 
			
		||||
 | 
			
		||||
        $qb->addOrderBy('event.date', 'DESC');
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function addFilters(array $filters, QueryBuilder $qb): void
 | 
			
		||||
    {
 | 
			
		||||
        if (($filters['q'] ?? '') !== '') {
 | 
			
		||||
            $qb->andWhere('event.name LIKE :content');
 | 
			
		||||
            $qb->setParameter('content', '%'.$filters['q'].'%');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (array_key_exists('dates', $filters)) {
 | 
			
		||||
            $dates = $filters['dates'];
 | 
			
		||||
            if (null !== ($dates['from'] ?? null)) {
 | 
			
		||||
                $qb->andWhere('event.date >= :date_from');
 | 
			
		||||
                $qb->setParameter('date_from', $dates['from']);
 | 
			
		||||
            }
 | 
			
		||||
            if (null !== ($dates['to'] ?? null)) {
 | 
			
		||||
                $qb->andWhere('event.date <= :date_to');
 | 
			
		||||
                $qb->setParameter('date_to', $dates['to']);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (0 < count($filters['event_types'] ?? [])) {
 | 
			
		||||
            $qb->andWhere('event.type IN (:event_types)');
 | 
			
		||||
            $qb->setParameter('event_types', $filters['event_types']);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildQueryByAllViewable(array $filters): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->entityManager->createQueryBuilder();
 | 
			
		||||
        $qb->from(Event::class, 'event');
 | 
			
		||||
 | 
			
		||||
        $aclConditions = $qb->expr()->orX();
 | 
			
		||||
 | 
			
		||||
        $i = 0;
 | 
			
		||||
        foreach ($this->authorizationHelperForCurrentUser->getReachableCenters(EventVoter::SEE) as $center) {
 | 
			
		||||
            foreach ($this->authorizationHelperForCurrentUser->getReachableScopes(EventVoter::SEE, $center) as $scopes) {
 | 
			
		||||
                $aclConditions->add(
 | 
			
		||||
                    $qb->expr()->andX(
 | 
			
		||||
                        'event.circle IN (:scopes_'.$i.')',
 | 
			
		||||
                        $qb->expr()->orX(
 | 
			
		||||
                            'event.center = :center_'.$i,
 | 
			
		||||
                            $qb->expr()->exists(
 | 
			
		||||
                                'SELECT 1 FROM '.Participation::class.' participation_'.$i.' JOIN participation_'.$i.'.event event_'.$i.
 | 
			
		||||
                                ' JOIN '.Person\PersonCenterHistory::class.' person_center_history_'.$i.
 | 
			
		||||
                                ' WITH IDENTITY(person_center_history_'.$i.'.person) = IDENTITY(participation_'.$i.'.person) '.
 | 
			
		||||
                                ' AND event_'.$i.'.date <= person_center_history_'.$i.'.startDate AND (person_center_history_'.$i.'.endDate IS NULL OR person_center_history_'.$i.'.endDate > event_'.$i.'.date) '.
 | 
			
		||||
                                ' WHERE participation_'.$i.'.event = event'
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
                $qb->setParameter('scopes_'.$i, $scopes);
 | 
			
		||||
                $qb->setParameter('center_'.$i, $center);
 | 
			
		||||
 | 
			
		||||
                ++$i;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (0 === $i) {
 | 
			
		||||
            $aclConditions->add('FALSE = TRUE');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb
 | 
			
		||||
            ->andWhere(
 | 
			
		||||
                $qb->expr()->orX(
 | 
			
		||||
                    'event.createdBy = :user',
 | 
			
		||||
                    $aclConditions
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        $qb->setParameter('user', $this->security->getUser());
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Entity\EventType;
 | 
			
		||||
 | 
			
		||||
interface EventACLAwareRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
 | 
			
		||||
     */
 | 
			
		||||
    public function countAllViewable(array $filters): int;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param array{q?: string, dates?: array{from?: \DateTimeImmutable|null, to?: \DateTimeImmutable|null}, event_types?: list<EventType>} $filters
 | 
			
		||||
     *
 | 
			
		||||
     * @return list<Event>
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllViewable(array $filters, int $offset = 0, int $limit = 50): array;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\EventType;
 | 
			
		||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\Persistence\ManagerRegistry;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @extends ServiceEntityRepository<EventType>
 | 
			
		||||
 */
 | 
			
		||||
final class EventTypeRepository extends ServiceEntityRepository
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        ManagerRegistry $registry,
 | 
			
		||||
        private readonly EntityManagerInterface $entityManager,
 | 
			
		||||
        private readonly TranslatorInterface $translator
 | 
			
		||||
    ) {
 | 
			
		||||
        parent::__construct($registry, EventType::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<EventType>
 | 
			
		||||
     */
 | 
			
		||||
    public function findAllActive(): array
 | 
			
		||||
    {
 | 
			
		||||
        $dql = 'SELECT et FROM '.EventType::class.' et WHERE et.active = TRUE ORDER BY JSON_EXTRACT(et.name, :lang)';
 | 
			
		||||
 | 
			
		||||
        return $this->entityManager->createQuery($dql)
 | 
			
		||||
            ->setParameter('lang', $this->translator->getLocale())
 | 
			
		||||
            ->getResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,15 +14,16 @@
 | 
			
		||||
 | 
			
		||||
    {{ form_row(edit_form.type, { 'label': 'Event type' }) }}
 | 
			
		||||
    {{ form_row(edit_form.moderator) }}
 | 
			
		||||
    {{ form_row(edit_form.location) }}
 | 
			
		||||
    {{ form_row(edit_form.organizationCost) }}
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
    {{ form_row(edit_form.comment) }}
 | 
			
		||||
    {{ form_row(edit_form.documents) }}
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions sticky-form-buttons">
 | 
			
		||||
        <li class="cancel">
 | 
			
		||||
 | 
			
		||||
            {% set returnPath = app.request.get('return_path') %}
 | 
			
		||||
            {% set returnLabel = app.request.get('return_label') %}
 | 
			
		||||
 | 
			
		||||
            <a href="{{ returnPath |default( path('chill_event_list_most_recent') ) }}" class="btn btn-cancel">
 | 
			
		||||
                {{ returnLabel |default('Back to the most recent events'|trans) }}
 | 
			
		||||
            <a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">
 | 
			
		||||
                {{ 'List of events'|trans|chill_return_path_label }}
 | 
			
		||||
            </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,85 +24,89 @@
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h2>{{ 'Events participation' |trans }}</h2>
 | 
			
		||||
 | 
			
		||||
<table class="table table-striped table-bordered border-dark align-middle mt-3 events">
 | 
			
		||||
    <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th class="chill-green">{{ 'Date'|trans }}</th>
 | 
			
		||||
            <th class="chill-red">{{ 'Name'|trans }}</th>
 | 
			
		||||
            <th class="chill-orange">{{ 'Event type'|trans }}</th>
 | 
			
		||||
            <th class="chill-red">{{ 'Role'|trans }}</th>
 | 
			
		||||
            <th class="chill-green">{{ 'Status'|trans }}</th>
 | 
			
		||||
            <th> </th>
 | 
			
		||||
        </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {% for participation in participations %}
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td>{{ participation.event.date|format_date('short') }}</td>
 | 
			
		||||
                <td>{{ participation.event.name }}</td>
 | 
			
		||||
                <td>{{ participation.event.type.name|localize_translatable_string }}</td>
 | 
			
		||||
                <td>{{ participation.role.name|localize_translatable_string }}</td>
 | 
			
		||||
                <td>{{ participation.status.name|localize_translatable_string }}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                    <div class="btn-group" role="group" aria-label="Button group actions">
 | 
			
		||||
    {% if participations|length == 0 %}
 | 
			
		||||
        <p class="chill-no-data-statement">{{ 'Any participation for this person'|trans }}</p>
 | 
			
		||||
    {% else %}
 | 
			
		||||
        <table class="table table-striped table-bordered border-dark align-middle mt-3 events">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th class="chill-green">{{ 'Date'|trans }}</th>
 | 
			
		||||
                    <th class="chill-red">{{ 'Name'|trans }}</th>
 | 
			
		||||
                    <th class="chill-orange">{{ 'Event type'|trans }}</th>
 | 
			
		||||
                    <th class="chill-red">{{ 'Role'|trans }}</th>
 | 
			
		||||
                    <th class="chill-green">{{ 'Status'|trans }}</th>
 | 
			
		||||
                    <th> </th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {% for participation in participations %}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>{{ participation.event.date|format_date('short') }}</td>
 | 
			
		||||
                        <td>{{ participation.event.name }}</td>
 | 
			
		||||
                        <td>{{ participation.event.type.name|localize_translatable_string }}</td>
 | 
			
		||||
                        <td>{{ participation.role.name|localize_translatable_string }}</td>
 | 
			
		||||
                        <td>{{ participation.status.name|localize_translatable_string }}</td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            <div class="btn-group" role="group" aria-label="Button group actions">
 | 
			
		||||
 | 
			
		||||
                        {% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
 | 
			
		||||
                        {% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
 | 
			
		||||
                                {% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
 | 
			
		||||
                                {% set returnLabel = 'Back to %person% events'|trans({ '%person%' : currentPerson } ) %}
 | 
			
		||||
 | 
			
		||||
                        {% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
 | 
			
		||||
                            <a  href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
 | 
			
		||||
                            class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
 | 
			
		||||
                            <i class="fa fa-fw fa-eye"></i>
 | 
			
		||||
                        </a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                                {% if is_granted('CHILL_EVENT_SEE_DETAILS', participation.event) %}
 | 
			
		||||
                                    <a  href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel } ) }}"
 | 
			
		||||
                                    class="btn btn-primary btn-sm" title="{{ 'See details of the event'|trans }}">
 | 
			
		||||
                                    <i class="fa fa-fw fa-eye"></i>
 | 
			
		||||
                                </a>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
 | 
			
		||||
                        {% if  is_granted('CHILL_EVENT_UPDATE', participation.event)
 | 
			
		||||
                           and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
 | 
			
		||||
                                {% if  is_granted('CHILL_EVENT_UPDATE', participation.event)
 | 
			
		||||
                                   and is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
 | 
			
		||||
 | 
			
		||||
                            <div class="btn-group" role="group">
 | 
			
		||||
                            <button class="btn btn-sm btn-warning dropdown-toggle" type="button" id="dropdownEdit" data-bs-toggle="dropdown" aria-expanded="false">
 | 
			
		||||
                                <i class="fa fa-pencil"></i>
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <ul class="dropdown-menu" aria-labelledby="dropdownEdit">
 | 
			
		||||
                                <li>
 | 
			
		||||
                                    <div class="btn-group" role="group">
 | 
			
		||||
                                    <button class="btn btn-sm btn-warning dropdown-toggle" type="button" id="dropdownEdit" data-bs-toggle="dropdown" aria-expanded="false">
 | 
			
		||||
                                        <i class="fa fa-pencil"></i>
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <ul class="dropdown-menu" aria-labelledby="dropdownEdit">
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a  href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                                class="dropdown-item">
 | 
			
		||||
                                                {{ 'Edit the event'|trans }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                        <li>
 | 
			
		||||
                                            <a  href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                                class="dropdown-item">
 | 
			
		||||
                                                {{ 'Edit the participation'|trans }}
 | 
			
		||||
                                            </a>
 | 
			
		||||
                                        </li>
 | 
			
		||||
                                    </ul>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {% else %}
 | 
			
		||||
 | 
			
		||||
                                    {% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
 | 
			
		||||
                                    <a  href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                        class="dropdown-item">
 | 
			
		||||
                                        class="btn btn-warning btn-sm">
 | 
			
		||||
                                        {{ 'Edit the event'|trans }}
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                                <li>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                    {% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
 | 
			
		||||
                                    <a  href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                        class="dropdown-item">
 | 
			
		||||
                                        class="btn btn-warning btn-sm">
 | 
			
		||||
                                        {{ 'Edit the participation'|trans }}
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                            {% if is_granted('CHILL_EVENT_UPDATE', participation.event) %}
 | 
			
		||||
                            <a  href="{{ path('chill_event__event_edit', { 'event_id' : participation.event.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                class="btn btn-warning btn-sm">
 | 
			
		||||
                                {{ 'Edit the event'|trans }}
 | 
			
		||||
                            </a>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            {% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
 | 
			
		||||
                            <a  href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id, 'return_path' : currentPath, 'return_label' : returnLabel }) }}"
 | 
			
		||||
                                class="btn btn-warning btn-sm">
 | 
			
		||||
                                {{ 'Edit the participation'|trans }}
 | 
			
		||||
                            </a>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    </div>
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
 | 
			
		||||
    </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
{% if participations|length < paginator.getTotalItems %}
 | 
			
		||||
    {{ chill_pagination(paginator) }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
{% extends '@ChillEvent/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_async_upload') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_async_upload') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block title 'Event creation'|trans %}
 | 
			
		||||
 | 
			
		||||
{% block event_content -%}
 | 
			
		||||
@@ -14,8 +22,13 @@
 | 
			
		||||
 | 
			
		||||
    {{ form_row(form.type, { 'label': 'Event type' }) }}
 | 
			
		||||
    {{ form_row(form.moderator) }}
 | 
			
		||||
    {{ form_row(form.location) }}
 | 
			
		||||
    {{ form_row(form.organizationCost) }}
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
    {{ form_row(form.comment) }}
 | 
			
		||||
    {{ form_row(form.documents) }}
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions sticky-form-buttons">
 | 
			
		||||
        <li class="cancel">
 | 
			
		||||
            <a href="{{ path('chill_event_list_most_recent') }}" class="btn btn-cancel">
 | 
			
		||||
                {{ 'Back to the most recent events'|trans }}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,92 @@
 | 
			
		||||
{% extends '@ChillEvent/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block title 'Events'|trans %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_pickentity_type') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_pickentity_type') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <h1>{{ block('title') }}</h1>
 | 
			
		||||
 | 
			
		||||
    {{ filter|chill_render_filter_order_helper }}
 | 
			
		||||
 | 
			
		||||
{# {% if is_granted('CHILL_EVENT_CREATE') %} #}
 | 
			
		||||
        <ul class="record_actions">
 | 
			
		||||
            <li><a class="btn btn-create" href="{{ chill_path_add_return_path('chill_event__event_new_pickcenter') }}">{{ 'Add an event'|trans }}</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
   {#  {% endif %} #}
 | 
			
		||||
    {% if events|length > 0 %}
 | 
			
		||||
        <div class="flex-table">
 | 
			
		||||
            {% for e in events %}
 | 
			
		||||
                <div class="item-bloc">
 | 
			
		||||
                    <div class="item-row">
 | 
			
		||||
                        <div class="item-col">
 | 
			
		||||
                            <div class="denomination h2">
 | 
			
		||||
                                {{ e.name }}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <p>{{ e.type.name|localize_translatable_string }}</p>
 | 
			
		||||
                            {% if e.moderator is not null %}
 | 
			
		||||
                                <p>{{ 'Moderator'|trans }}: {{ e.moderator|chill_entity_render_box }}</p>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="item-col">
 | 
			
		||||
                            <div class="container" style="text-align: right;">
 | 
			
		||||
                                <p>{{ e.date|format_datetime('medium', 'medium') }}</p>
 | 
			
		||||
                                <p>{{ 'count participations to this event'|trans({'count': e.participations|length}) }}</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% if e.participations|length > 0 %}
 | 
			
		||||
                        <div class="item-row separator">
 | 
			
		||||
                            <strong>{{ 'Participations'|trans }} : </strong>
 | 
			
		||||
                            {% for part in e.participations|slice(0, 20) %}
 | 
			
		||||
                                {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
 | 
			
		||||
                                    targetEntity: { name: 'person', id: part.person.id },
 | 
			
		||||
                                    action: 'show',
 | 
			
		||||
                                    displayBadge: true,
 | 
			
		||||
                                    buttonText: part.person|chill_entity_render_string,
 | 
			
		||||
                                    isDead: part.person.deathdate is not null
 | 
			
		||||
                                } %}
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                            {% if e.participations|length > 20 %}
 | 
			
		||||
                                {{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <div class="item-row">
 | 
			
		||||
                        <div class="item-col">
 | 
			
		||||
                            {{ form_start(eventForms[e.id]) }}
 | 
			
		||||
                            {{ form_widget(eventForms[e.id].person_id) }}
 | 
			
		||||
                            {{ form_end(eventForms[e.id]) }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="item-row separator">
 | 
			
		||||
                        <div class="item-col item-meta">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="item-col">
 | 
			
		||||
                            <ul class="record_actions">
 | 
			
		||||
                                {% if is_granted('CHILL_EVENT_UPDATE', e) %}
 | 
			
		||||
                                    <li><a href="{{ chill_path_add_return_path('chill_event__event_delete', {'event_id': e.id}) }}" class="btn btn-delete"></a></li>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                                {% if is_granted('CHILL_EVENT_UPDATE', e) %}
 | 
			
		||||
                                    <li><a href="{{ chill_path_add_return_path('chill_event__event_edit', {'event_id': e.id}) }}" class="btn btn-edit"></a></li>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                                <li><a href="{{ chill_path_add_return_path('chill_event__event_show', {'event_id': e.id}) }}" class="btn btn-show"></a></li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {{ chill_pagination(pagination) }}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -4,12 +4,28 @@
 | 
			
		||||
 | 
			
		||||
{% import '@ChillPerson/Person/macro.html.twig' as person_macro %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_pickentity_type') }}
 | 
			
		||||
    {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_pickentity_type') }}
 | 
			
		||||
    {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block event_content -%}
 | 
			
		||||
<div class="col-10">
 | 
			
		||||
    <h1>{{ 'Details of an event'|trans }}</h1>
 | 
			
		||||
 | 
			
		||||
    <table class="table table-bordered border-dark align-middle">
 | 
			
		||||
        <tbody>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'Circle'|trans }}</th>
 | 
			
		||||
                <td>{{ event.circle.name|localize_translatable_string }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'Name'|trans }}</th>
 | 
			
		||||
                <td>{{ event.name }}</td>
 | 
			
		||||
@@ -22,42 +38,62 @@
 | 
			
		||||
                <th>{{ 'Event type'|trans }}</th>
 | 
			
		||||
                <td>{{ event.type.name|localize_translatable_string }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'Circle'|trans }}</th>
 | 
			
		||||
                <td>{{ event.circle.name|localize_translatable_string }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'Moderator'|trans }}</th>
 | 
			
		||||
                <td>{{ event.moderator|trans|default('-') }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'event.fields.organizationCost'|trans }}</th>
 | 
			
		||||
                <td>{{ event.organizationCost|format_currency('EUR') }}</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>{{ 'event.fields.location'|trans }}</th>
 | 
			
		||||
                <td>
 | 
			
		||||
                    {% if event.location is not null %}
 | 
			
		||||
                        {{ event.location.name }}
 | 
			
		||||
                        {% if event.location.address is not same as(null) %}{{ event.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (event.location.name is empty), 'details_button': true}) }}{% endif %}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <span class="chill-no-data-statement">{{ 'Any location for this event'|trans }}</span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
    {% if event.documents|length > 0 %}
 | 
			
		||||
        <div>
 | 
			
		||||
            <p><strong>{{ 'event.fields.documents'|trans }}</strong></p>
 | 
			
		||||
            <ul>
 | 
			
		||||
                {% for d in event.documents %}
 | 
			
		||||
                    <li class="document-list-item">{{ d.title|chill_print_or_message('document.Any title') }} {{ d|chill_document_button_group(d.title, is_granted('CHILL_EVENT_SEE_DETAILS', event), {small: false}) }}</li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% if not event.comment.empty %}
 | 
			
		||||
        <div>
 | 
			
		||||
            {{ event.comment|chill_entity_render_box({
 | 
			
		||||
                'disable_markdown': false,
 | 
			
		||||
                'metadata': true,
 | 
			
		||||
            }) }}
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
 | 
			
		||||
        {% set returnPath = app.request.get('return_path') %}
 | 
			
		||||
        {% set returnLabel = app.request.get('return_label') %}
 | 
			
		||||
 | 
			
		||||
        {% if returnPath and returnLabel %}
 | 
			
		||||
            <li class="cancel">
 | 
			
		||||
                <a href="{{ returnPath }}" class="btn btn-cancel">{{ returnLabel }}</a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
                <a href="{{ path('chill_event__event_edit', {
 | 
			
		||||
                    'event_id': event.id,
 | 
			
		||||
                    'return_path': app.request.getRequestUri,
 | 
			
		||||
                    'return_label': 'Back to details of the event'|trans
 | 
			
		||||
                    }) }}" class="btn btn-edit">{{ 'Edit'|trans }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
        {% else %}
 | 
			
		||||
            <li>
 | 
			
		||||
                <a href="{{ path('chill_event__event_edit', {'event_id': event.id }) }}" class="btn btn-edit">
 | 
			
		||||
                    {{ 'Edit'|trans }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </li>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <li class="cancel">
 | 
			
		||||
            <a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">{{ 'Back to the list'|trans|chill_return_path_label }}</a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
            <a href="{{ chill_path_add_return_path('chill_event__event_edit', {'event_id': event.id }, false, 'See'|trans) }}" class="btn btn-edit">
 | 
			
		||||
                {{ 'Edit'|trans }}
 | 
			
		||||
            </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
            <a href="{{ path('chill_event__event_delete', {'event_id' : event.id } ) }}"
 | 
			
		||||
               class="btn btn-delete">{{ 'Delete event'|trans }}</a>
 | 
			
		||||
@@ -83,7 +119,15 @@
 | 
			
		||||
            <tbody>
 | 
			
		||||
            {% for participation in event.participations %}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>{{ person_macro.render(participation.person) }}</td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
 | 
			
		||||
                            targetEntity: { name: 'person', id: participation.person.id },
 | 
			
		||||
                            action: 'show',
 | 
			
		||||
                            displayBadge: true,
 | 
			
		||||
                            buttonText: participation.person|chill_entity_render_string,
 | 
			
		||||
                            isDead: participation.person.deathdate is not null
 | 
			
		||||
                        } %}
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>{{ participation.role.name|localize_translatable_string }}</td>
 | 
			
		||||
                    <td>{{ participation.status.name|localize_translatable_string }}</td>
 | 
			
		||||
                    <td>{{ participation.lastUpdate|ago }} {# sf4 check: filter 'time_diff' is abandoned,
 | 
			
		||||
@@ -94,7 +138,7 @@
 | 
			
		||||
                        <ul class="record_actions">
 | 
			
		||||
                            {% if is_granted('CHILL_EVENT_PARTICIPATION_UPDATE', participation) %}
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a href="{{ path('chill_event_participation_edit', { 'participation_id' : participation.id } ) }}"
 | 
			
		||||
                                <a href="{{ chill_path_add_return_path('chill_event_participation_edit', { 'participation_id' : participation.id }, false, 'See'|trans ) }}"
 | 
			
		||||
                                   class="btn btn-edit" title="{{ 'Edit'|trans }}"></a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li>
 | 
			
		||||
@@ -126,11 +170,8 @@
 | 
			
		||||
                    'class' : 'custom-select',
 | 
			
		||||
                    'style': 'min-width: 15em; max-width: 18em; display: inline-block;'
 | 
			
		||||
                }} ) }}
 | 
			
		||||
                <div class="input-group-append">
 | 
			
		||||
                    {{ form_widget(form_add_participation_by_person.submit, { 'attr' : { 'class' : 'btn btn-create' } } ) }}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {{ form_rest(form_add_participation_by_person) }}
 | 
			
		||||
            <input type="hidden" name="returnPath" value="{{ app.request.requestUri }}" />
 | 
			
		||||
            {{ form_end(form_add_participation_by_person) }}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
    <ul class="record_actions sticky-form-buttons">
 | 
			
		||||
        <li class="cancel">
 | 
			
		||||
            <a href="{{ path('chill_event__event_show', { 'event_id' : participation.event.id } ) }}" class="btn btn-cancel">
 | 
			
		||||
                {{ 'Back to the event'|trans }}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class EventListControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    private readonly PaginatorFactory $paginatorFactory;
 | 
			
		||||
    private readonly Environment $environment;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testList(): void
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
 | 
			
		||||
        $client->request('GET', '/fr/event/event/list');
 | 
			
		||||
        self::assertResponseIsSuccessful();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,11 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Entity\Event;
 | 
			
		||||
use Chill\EventBundle\Repository\EventRepository;
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Chill\PersonBundle\DataFixtures\Helper\PersonRandomHelper;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
use function count;
 | 
			
		||||
 | 
			
		||||
@@ -23,15 +28,12 @@ use function count;
 | 
			
		||||
 */
 | 
			
		||||
final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var \Symfony\Component\BrowserKit\AbstractBrowser
 | 
			
		||||
     */
 | 
			
		||||
    protected $client;
 | 
			
		||||
    use PersonRandomHelper;
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var \Doctrine\ORM\EntityManagerInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $em;
 | 
			
		||||
    private EntityManagerInterface $em;
 | 
			
		||||
 | 
			
		||||
    private EventRepository $eventRepository;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Keep a cache for each person id given by the function getRandomPerson.
 | 
			
		||||
@@ -44,23 +46,21 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
     */
 | 
			
		||||
    private array $personsIdsCache = [];
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    protected function prepareDI(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
 | 
			
		||||
        $this->client = self::createClient([], [
 | 
			
		||||
            'PHP_AUTH_USER' => 'center a_social',
 | 
			
		||||
            'PHP_AUTH_PW' => 'password',
 | 
			
		||||
            'HTTP_ACCEPT_LANGUAGE' => 'fr_FR',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $container = self::$kernel->getContainer();
 | 
			
		||||
 | 
			
		||||
        $this->em = $container->get('doctrine.orm.entity_manager');
 | 
			
		||||
        $this->em = self::$container->get(EntityManagerInterface::class);
 | 
			
		||||
        $this->eventRepository = self::$container->get(EventRepository::class);
 | 
			
		||||
 | 
			
		||||
        $this->personsIdsCache = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function tearDown(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::tearDown();
 | 
			
		||||
 | 
			
		||||
        self::ensureKernelShutdown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method test participation creation with wrong parameters.
 | 
			
		||||
     *
 | 
			
		||||
@@ -68,11 +68,13 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
     */
 | 
			
		||||
    public function testCreateActionWrongParameters()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEvent();
 | 
			
		||||
        $person = $this->getRandomPerson();
 | 
			
		||||
        $person = $this->getRandomPerson($this->em);
 | 
			
		||||
 | 
			
		||||
        // missing person_id or persons_ids
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/create',
 | 
			
		||||
            [
 | 
			
		||||
@@ -81,33 +83,33 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/create fail if '
 | 
			
		||||
                .'both person_id and persons_ids are missing'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // having both person_id and persons_ids
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/create',
 | 
			
		||||
            [
 | 
			
		||||
                'event_id' => $event->getId(),
 | 
			
		||||
                'persons_ids' => implode(',', [
 | 
			
		||||
                    $this->getRandomPerson()->getId(),
 | 
			
		||||
                    $this->getRandomPerson()->getId(),
 | 
			
		||||
                    $this->getRandomPerson($this->em)->getId(),
 | 
			
		||||
                    $this->getRandomPerson($this->em)->getId(),
 | 
			
		||||
                ]),
 | 
			
		||||
                'person_id' => $person->getId(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/create fail if both person_id and '
 | 
			
		||||
                .'persons_ids are set'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // missing event_id
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/create',
 | 
			
		||||
            [
 | 
			
		||||
@@ -116,12 +118,12 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/create fails if event_id is missing'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // persons_ids with wrong content
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/create',
 | 
			
		||||
            [
 | 
			
		||||
@@ -131,42 +133,47 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/create fails if persons_ids has wrong content'
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testEditMultipleAction()
 | 
			
		||||
    {
 | 
			
		||||
        /** @var \Chill\EventBundle\Entity\Event $event */
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
 | 
			
		||||
        /** @var Event $event */
 | 
			
		||||
        $event = $this->getRandomEventWithMultipleParticipations();
 | 
			
		||||
 | 
			
		||||
        $crawler = $this->client->request('GET', '/fr/event/participation/'.$event->getId().
 | 
			
		||||
        $crawler = $client->request('GET', '/fr/event/participation/'.$event->getId().
 | 
			
		||||
                '/edit_multiple');
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
 | 
			
		||||
        $this->assertEquals(200, $client->getResponse()->getStatusCode());
 | 
			
		||||
 | 
			
		||||
        $button = $crawler->selectButton('Mettre à jour');
 | 
			
		||||
        $this->assertEquals(1, $button->count(), "test the form with button 'mettre à jour' exists ");
 | 
			
		||||
 | 
			
		||||
        $this->client->submit($button->form(), [
 | 
			
		||||
        $client->submit($button->form(), [
 | 
			
		||||
            'form[participations][0][role]' => $event->getType()->getRoles()->first()->getId(),
 | 
			
		||||
            'form[participations][0][status]' => $event->getType()->getStatuses()->first()->getId(),
 | 
			
		||||
            'form[participations][1][role]' => $event->getType()->getRoles()->last()->getId(),
 | 
			
		||||
            'form[participations][1][status]' => $event->getType()->getStatuses()->last()->getId(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($this->client->getResponse()
 | 
			
		||||
        $this->assertTrue($client->getResponse()
 | 
			
		||||
            ->isRedirect('/fr/event/event/'.$event->getId().'/show'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNewActionWrongParameters()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEvent();
 | 
			
		||||
        $person = $this->getRandomPerson();
 | 
			
		||||
        $person = $this->getRandomPerson($this->em);
 | 
			
		||||
 | 
			
		||||
        // missing person_id or persons_ids
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -175,33 +182,33 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/new fail if '
 | 
			
		||||
                .'both person_id and persons_ids are missing'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // having both person_id and persons_ids
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
                'event_id' => $event->getId(),
 | 
			
		||||
                'persons_ids' => implode(',', [
 | 
			
		||||
                    $this->getRandomPerson()->getId(),
 | 
			
		||||
                    $this->getRandomPerson()->getId(),
 | 
			
		||||
                    $this->getRandomPerson($this->em)->getId(),
 | 
			
		||||
                    $this->getRandomPerson($this->em)->getId(),
 | 
			
		||||
                ]),
 | 
			
		||||
                'person_id' => $person->getId(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/new fail if both person_id and '
 | 
			
		||||
                .'persons_ids are set'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // missing event_id
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -210,12 +217,12 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/new fails if event_id is missing'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // persons_ids with wrong content
 | 
			
		||||
        $this->client->request(
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -225,13 +232,15 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            400,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'Test that /fr/event/participation/new fails if persons_ids has wrong content'
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNewMultipleAction()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEvent();
 | 
			
		||||
        // record the number of participation for the event (used later in this test)
 | 
			
		||||
        $nbParticipations = $event->getParticipations()->count();
 | 
			
		||||
@@ -244,10 +253,10 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
                ->toArray()
 | 
			
		||||
        );
 | 
			
		||||
        // get some random people
 | 
			
		||||
        $person1 = $this->getRandomPerson();
 | 
			
		||||
        $person2 = $this->getRandomPerson();
 | 
			
		||||
        $person1 = $this->getRandomPerson($this->em);
 | 
			
		||||
        $person2 = $this->getRandomPerson($this->em);
 | 
			
		||||
 | 
			
		||||
        $crawler = $this->client->request(
 | 
			
		||||
        $crawler = $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -258,7 +267,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            200,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/new is successful'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -266,7 +275,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertNotNull($button, "test the form with button 'Créer' exists");
 | 
			
		||||
 | 
			
		||||
        $this->client->submit($button->form(), [
 | 
			
		||||
        $client->submit($button->form(), [
 | 
			
		||||
            'form' => [
 | 
			
		||||
                'participations' => [
 | 
			
		||||
                    0 => [
 | 
			
		||||
@@ -281,8 +290,8 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($this->client->getResponse()->isRedirect());
 | 
			
		||||
        $crawler = $this->client->followRedirect();
 | 
			
		||||
        $this->assertTrue($client->getResponse()->isRedirect());
 | 
			
		||||
        $crawler = $client->followRedirect();
 | 
			
		||||
 | 
			
		||||
        $span1 = $crawler->filter('table td span.entity-person a:contains("'
 | 
			
		||||
                .$person1->getFirstName().'"):contains("'.$person1->getLastname().'")');
 | 
			
		||||
@@ -292,7 +301,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        $this->assertGreaterThan(0, \count($span2));
 | 
			
		||||
 | 
			
		||||
        // as the container has reloaded, reload the event
 | 
			
		||||
        $event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
 | 
			
		||||
        $event = $this->em->getRepository(Event::class)->find($event->getId());
 | 
			
		||||
        $this->em->refresh($event);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals($nbParticipations + 2, $event->getParticipations()->count());
 | 
			
		||||
@@ -300,13 +309,15 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
    public function testNewMultipleWithAllPeopleParticipating()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEventWithMultipleParticipations();
 | 
			
		||||
 | 
			
		||||
        $persons_id = implode(',', $event->getParticipations()->map(
 | 
			
		||||
            static fn ($p) => $p->getPerson()->getId()
 | 
			
		||||
        )->toArray());
 | 
			
		||||
 | 
			
		||||
        $crawler = $this->client->request(
 | 
			
		||||
        $crawler = $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -317,13 +328,15 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            302,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/new is redirecting'
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testNewMultipleWithSomePeopleParticipating()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEventWithMultipleParticipations();
 | 
			
		||||
        // record the number of participation for the event (used later in this test)
 | 
			
		||||
        $nbParticipations = $event->getParticipations()->count();
 | 
			
		||||
@@ -335,12 +348,12 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        $this->personsIdsCache = array_merge($this->personsIdsCache, $persons_id);
 | 
			
		||||
 | 
			
		||||
        // get a random person
 | 
			
		||||
        $newPerson = $this->getRandomPerson();
 | 
			
		||||
        $newPerson = $this->getRandomPerson($this->em);
 | 
			
		||||
 | 
			
		||||
        // build the `persons_ids` parameter
 | 
			
		||||
        $persons_ids_string = implode(',', [...$persons_id, $newPerson->getId()]);
 | 
			
		||||
 | 
			
		||||
        $crawler = $this->client->request(
 | 
			
		||||
        $crawler = $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -351,7 +364,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            200,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/new is successful'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -377,15 +390,15 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        $this->assertNotNull($button, "test the form with button 'Créer' exists");
 | 
			
		||||
 | 
			
		||||
        // submit the form
 | 
			
		||||
        $this->client->submit($button->form(), [
 | 
			
		||||
        $client->submit($button->form(), [
 | 
			
		||||
            'participation[role]' => $event->getType()->getRoles()->first()->getId(),
 | 
			
		||||
            'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($this->client->getResponse()->isRedirect());
 | 
			
		||||
        $this->assertTrue($client->getResponse()->isRedirect());
 | 
			
		||||
 | 
			
		||||
        // reload the event and test there is a new participation
 | 
			
		||||
        $event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
 | 
			
		||||
        $event = $this->em->getRepository(Event::class)
 | 
			
		||||
            ->find($event->getId());
 | 
			
		||||
        $this->em->refresh($event);
 | 
			
		||||
 | 
			
		||||
@@ -398,12 +411,14 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
    public function testNewSingleAction()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
        $this->prepareDI();
 | 
			
		||||
        $event = $this->getRandomEvent();
 | 
			
		||||
        // record the number of participation for the event
 | 
			
		||||
        $nbParticipations = $event->getParticipations()->count();
 | 
			
		||||
        $person = $this->getRandomPerson();
 | 
			
		||||
        $person = $this->getRandomPerson($this->em);
 | 
			
		||||
 | 
			
		||||
        $crawler = $this->client->request(
 | 
			
		||||
        $crawler = $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/fr/event/participation/new',
 | 
			
		||||
            [
 | 
			
		||||
@@ -414,7 +429,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(
 | 
			
		||||
            200,
 | 
			
		||||
            $this->client->getResponse()->getStatusCode(),
 | 
			
		||||
            $client->getResponse()->getStatusCode(),
 | 
			
		||||
            'test that /fr/event/participation/new is successful'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
@@ -422,13 +437,13 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertNotNull($button, "test the form with button 'Créer' exists");
 | 
			
		||||
 | 
			
		||||
        $this->client->submit($button->form(), [
 | 
			
		||||
        $client->submit($button->form(), [
 | 
			
		||||
            'participation[role]' => $event->getType()->getRoles()->first()->getId(),
 | 
			
		||||
            'participation[status]' => $event->getType()->getStatuses()->first()->getId(),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->assertTrue($this->client->getResponse()->isRedirect());
 | 
			
		||||
        $crawler = $this->client->followRedirect();
 | 
			
		||||
        $this->assertTrue($client->getResponse()->isRedirect());
 | 
			
		||||
        $crawler = $client->followRedirect();
 | 
			
		||||
 | 
			
		||||
        $span = $crawler->filter('table td span.entity-person a:contains("'
 | 
			
		||||
                .$person->getFirstName().'"):contains("'.$person->getLastname().'")');
 | 
			
		||||
@@ -436,29 +451,23 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
        $this->assertGreaterThan(0, \count($span));
 | 
			
		||||
 | 
			
		||||
        // as the container has reloaded, reload the event
 | 
			
		||||
        $event = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)->find($event->getId());
 | 
			
		||||
        $event = $this->em->getRepository(Event::class)->find($event->getId());
 | 
			
		||||
        $this->em->refresh($event);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals($nbParticipations + 1, $event->getParticipations()->count());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return \Chill\EventBundle\Entity\Event
 | 
			
		||||
     */
 | 
			
		||||
    protected function getRandomEvent(mixed $centerName = 'Center A', mixed $circleName = 'social')
 | 
			
		||||
    private function getRandomEvent(string $centerName = 'Center A', string $circleName = 'social'): Event
 | 
			
		||||
    {
 | 
			
		||||
        $center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
 | 
			
		||||
            ->findByName($centerName);
 | 
			
		||||
        $dql = 'FROM '.Event::class.' e JOIN e.center center JOIN e.circle scope WHERE center.name LIKE :cname AND JSON_EXTRACT(scope.name, \'fr\') LIKE :sname';
 | 
			
		||||
 | 
			
		||||
        $circles = $this->em->getRepository(\Chill\MainBundle\Entity\Scope::class)
 | 
			
		||||
            ->findAll();
 | 
			
		||||
        array_filter($circles, static fn ($circle) => \in_array($circleName, $circle->getName(), true));
 | 
			
		||||
        $circle = $circles[0];
 | 
			
		||||
        $ids = $this->em->createQuery(
 | 
			
		||||
            'SELECT DISTINCT e.id '.$dql
 | 
			
		||||
        )
 | 
			
		||||
            ->setParameters(['cname' => $centerName, 'sname' => $circleName])
 | 
			
		||||
            ->getResult();
 | 
			
		||||
 | 
			
		||||
        $events = $this->em->getRepository(\Chill\EventBundle\Entity\Event::class)
 | 
			
		||||
            ->findBy(['center' => $center, 'circle' => $circle]);
 | 
			
		||||
 | 
			
		||||
        return $events[array_rand($events)];
 | 
			
		||||
        return $this->eventRepository->find($ids[array_rand($ids)]['id']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -467,7 +476,7 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
     * @param string $centerName
 | 
			
		||||
     * @param type   $circleName
 | 
			
		||||
     *
 | 
			
		||||
     * @return \Chill\EventBundle\Entity\Event
 | 
			
		||||
     * @return Event
 | 
			
		||||
     */
 | 
			
		||||
    protected function getRandomEventWithMultipleParticipations(
 | 
			
		||||
        $centerName = 'Center A',
 | 
			
		||||
@@ -479,35 +488,4 @@ final class ParticipationControllerTest extends WebTestCase
 | 
			
		||||
                $event :
 | 
			
		||||
                $this->getRandomEventWithMultipleParticipations($centerName, $circleName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a person randomly.
 | 
			
		||||
     *
 | 
			
		||||
     * This function does not give the same person twice
 | 
			
		||||
     * for each test.
 | 
			
		||||
     *
 | 
			
		||||
     * You may ask to ignore some people by adding their id to the property
 | 
			
		||||
     * `$this->personsIdsCache`
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $centerName
 | 
			
		||||
     *
 | 
			
		||||
     * @return \Chill\PersonBundle\Entity\Person
 | 
			
		||||
     */
 | 
			
		||||
    protected function getRandomPerson($centerName = 'Center A')
 | 
			
		||||
    {
 | 
			
		||||
        $center = $this->em->getRepository(\Chill\MainBundle\Entity\Center::class)
 | 
			
		||||
            ->findByName($centerName);
 | 
			
		||||
 | 
			
		||||
        $persons = $this->em->getRepository(\Chill\PersonBundle\Entity\Person::class)
 | 
			
		||||
            ->findBy(['center' => $center]);
 | 
			
		||||
 | 
			
		||||
        $person = $persons[array_rand($persons)];
 | 
			
		||||
 | 
			
		||||
        if (\in_array($person->getId(), $this->personsIdsCache, true)) {
 | 
			
		||||
            return $this->getRandomPerson($centerName); // we try another time
 | 
			
		||||
        }
 | 
			
		||||
        $this->personsIdsCache[] = $person->getId();
 | 
			
		||||
 | 
			
		||||
        return $person;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,97 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\EventBundle\Tests\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\EventBundle\Repository\EventACLAwareRepository;
 | 
			
		||||
use Chill\EventBundle\Security\Authorization\EventVoter;
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Entity\Scope;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class EventACLAwareRepositoryTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateFilters
 | 
			
		||||
     *
 | 
			
		||||
     * @throws \Doctrine\ORM\NoResultException
 | 
			
		||||
     * @throws \Doctrine\ORM\NonUniqueResultException
 | 
			
		||||
     */
 | 
			
		||||
    public function testCountAllViewable(array $filters): void
 | 
			
		||||
    {
 | 
			
		||||
        $repository = $this->buildEventACLAwareRepository();
 | 
			
		||||
 | 
			
		||||
        $this->assertGreaterThanOrEqual(0, $repository->countAllViewable($filters));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateFilters
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindAllViewable(array $filters): void
 | 
			
		||||
    {
 | 
			
		||||
        $repository = $this->buildEventACLAwareRepository();
 | 
			
		||||
 | 
			
		||||
        $this->assertIsArray($repository->findAllViewable($filters));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function generateFilters(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [[]];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildEventACLAwareRepository(): EventACLAwareRepository
 | 
			
		||||
    {
 | 
			
		||||
        $em = self::$container->get(EntityManagerInterface::class);
 | 
			
		||||
        $user = $em->createQuery('SELECT u FROM '.User::class.' u')
 | 
			
		||||
            ->setMaxResults(1)
 | 
			
		||||
            ->getSingleResult()
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $scopes = $em->createQuery('SELECT s FROM '.Scope::class.' s')
 | 
			
		||||
            ->setMaxResults(3)
 | 
			
		||||
            ->getResult();
 | 
			
		||||
 | 
			
		||||
        $centers = $em->createQuery('SELECT c FROM '.Center::class.' c')
 | 
			
		||||
            ->setMaxResults(3)
 | 
			
		||||
            ->getResult();
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->getUser()->willReturn($user);
 | 
			
		||||
 | 
			
		||||
        $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class);
 | 
			
		||||
        $authorizationHelper->getReachableCenters(EventVoter::SEE)->willReturn($centers);
 | 
			
		||||
        $authorizationHelper->getReachableScopes(EventVoter::SEE, Argument::type(Center::class))->willReturn($scopes);
 | 
			
		||||
 | 
			
		||||
        return new EventACLAwareRepository(
 | 
			
		||||
            $authorizationHelper->reveal(),
 | 
			
		||||
            $em,
 | 
			
		||||
            $security->reveal()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
 | 
			
		||||
    Chill\EventBundle\Controller\EventController:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
 | 
			
		||||
            $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
 | 
			
		||||
            $formFactoryInterface: '@Symfony\Component\Form\FormFactoryInterface'
 | 
			
		||||
            $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
 | 
			
		||||
            $paginator: '@chill_main.paginator_factory'
 | 
			
		||||
        public: true
 | 
			
		||||
        tags: ['controller.service_arguments']
 | 
			
		||||
 | 
			
		||||
    Chill\EventBundle\Controller\ParticipationController:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $logger: '@Psr\Log\LoggerInterface'
 | 
			
		||||
        tags: ['controller.service_arguments']
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
    Chill\EventBundle\Menu\PersonMenuBuilder:
 | 
			
		||||
        arguments:
 | 
			
		||||
            $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
 | 
			
		||||
            $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: 'chill.menu_builder' }
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
Chill\EventBundle\Entity\Participation:
 | 
			
		||||
    properties:
 | 
			
		||||
        event: 
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
        status:
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
        person:
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
    constraints:
 | 
			
		||||
        - Callback: isConsistent
 | 
			
		||||
        
 | 
			
		||||
       
 | 
			
		||||
Chill\EventBundle\Entity\Event:
 | 
			
		||||
    properties:
 | 
			
		||||
        name:
 | 
			
		||||
            - Length:
 | 
			
		||||
                min: 3
 | 
			
		||||
                max: 75
 | 
			
		||||
                minMessage: The event name must have at least {{ limit }} characters.
 | 
			
		||||
                maxMessage: The event name must have maximum {{ limit }} characters.
 | 
			
		||||
        type:
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
        circle:
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
        center:
 | 
			
		||||
            - NotNull: ~
 | 
			
		||||
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 */
 | 
			
		||||
class Version20160318111334 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'initialize the bundle chill event';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this down() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_role DROP CONSTRAINT FK_AA714E54C54C8C93');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_status DROP CONSTRAINT FK_A6CC85D0C54C8C93');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP CONSTRAINT FK_4E7768ACD60322AC');
 | 
			
		||||
@@ -50,9 +52,6 @@ class Version20160318111334 extends AbstractMigration
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this up() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_event_event_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_event_role_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_event_status_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
@@ -123,11 +122,26 @@ class Version20160318111334 extends AbstractMigration
 | 
			
		||||
                .'FOREIGN KEY (event_id) '
 | 
			
		||||
                .'REFERENCES chill_event_event (id) '
 | 
			
		||||
                .'NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation '
 | 
			
		||||
 | 
			
		||||
        // before adding fk constraint to person, check what is the table name
 | 
			
		||||
        $results = $this->connection->executeQuery('SELECT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = \'chill_person_person\')');
 | 
			
		||||
        /** @var bool $isChillPersonPersonTable */
 | 
			
		||||
        $isChillPersonPersonTable = $results->fetchFirstColumn()[0];
 | 
			
		||||
 | 
			
		||||
        if ($isChillPersonPersonTable) {
 | 
			
		||||
            $this->addSql('ALTER TABLE chill_event_participation '
 | 
			
		||||
                .'ADD CONSTRAINT FK_4E7768AC217BBB47 '
 | 
			
		||||
                .'FOREIGN KEY (person_id) '
 | 
			
		||||
                .'REFERENCES chill_person_person (id) '
 | 
			
		||||
                .'NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->addSql('ALTER TABLE chill_event_participation '
 | 
			
		||||
                .'ADD CONSTRAINT FK_4E7768AC217BBB47 '
 | 
			
		||||
                .'FOREIGN KEY (person_id) '
 | 
			
		||||
                .'REFERENCES Person (id) '
 | 
			
		||||
                .'NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation '
 | 
			
		||||
                .'ADD CONSTRAINT FK_4E7768ACD60322AC '
 | 
			
		||||
                .'FOREIGN KEY (role_id) '
 | 
			
		||||
 
 | 
			
		||||
@@ -19,18 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 */
 | 
			
		||||
final class Version20190110140538 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'switch event date to datetime';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ALTER date TYPE DATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ALTER date TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ALTER date DROP DEFAULT');
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,13 @@ use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 */
 | 
			
		||||
final class Version20190115140042 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'add a moderator field to events';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this down() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
 | 
			
		||||
        $this->addSql('DROP INDEX IDX_FA320FC8D0AFA354');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP moderator_id');
 | 
			
		||||
@@ -31,9 +33,6 @@ final class Version20190115140042 extends AbstractMigration
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this up() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD moderator_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_FA320FC8D0AFA354 ON chill_event_event (moderator_id)');
 | 
			
		||||
 
 | 
			
		||||
@@ -19,20 +19,19 @@ use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 */
 | 
			
		||||
final class Version20190201143121 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'fix moderator: relation with user (not person)';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this down() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT fk_fa320fc8d0afa354');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT fk_fa320fc8d0afa354 FOREIGN KEY (moderator_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        // this up() migration is auto-generated, please modify it to your needs
 | 
			
		||||
        $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP CONSTRAINT FK_FA320FC8D0AFA354');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC8D0AFA354 FOREIGN KEY (moderator_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Event;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20231127134244 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'add creation - update information on event and event participation';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD createdBy_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD updatedBy_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_event_event.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_event_event.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC83174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC865FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_FA320FC83174800F ON chill_event_event (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_FA320FC865FF1AEC ON chill_event_event (updatedBy_id)');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD createdBy_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD updatedBy_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_event_participation.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_event_participation.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD CONSTRAINT FK_4E7768AC65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_4E7768AC3174800F ON chill_event_participation (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_4E7768AC65FF1AEC ON chill_event_participation (updatedBy_id)');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP createdAt');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP updatedAt');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP createdBy_id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP updatedBy_id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP createdAt');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP updatedAt');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP createdBy_id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP updatedBy_id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Event;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20231128114959 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add unique index on participation and drop column participation::lastUpdate';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('UPDATE chill_event_participation SET updatedAt=lastupdate WHERE updatedat IS NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation DROP lastupdate');
 | 
			
		||||
        $this->addSql('WITH ordering AS (SELECT id, event_id, person_id, rank() OVER (PARTITION BY event_id, person_id ORDER BY id DESC) as ranked FROM chill_event_participation),
 | 
			
		||||
     not_last AS (SELECT * FROM ordering where ranked > 1)
 | 
			
		||||
     DELETE FROM chill_event_participation WHERE id IN (select id FROM not_last)');
 | 
			
		||||
        $this->addSql('CREATE UNIQUE INDEX chill_event_participation_event_person_unique_idx ON chill_event_participation (event_id, person_id)');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP INDEX chill_event_participation_event_person_unique_idx');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_participation ADD lastupdate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL');
 | 
			
		||||
        $this->addSql('UPDATE chill_event_participation set lastupdate = updatedat');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Event;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20231128122635 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Append more fields on event: location, documents, and comment';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_event_event_documents (event_id INT NOT NULL, storedobject_id INT NOT NULL, PRIMARY KEY(event_id, storedobject_id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_5C1B638671F7E88B ON chill_event_event_documents (event_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_5C1B6386EE684399 ON chill_event_event_documents (storedobject_id)');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B638671F7E88B FOREIGN KEY (event_id) REFERENCES chill_event_event (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event_documents ADD CONSTRAINT FK_5C1B6386EE684399 FOREIGN KEY (storedobject_id) REFERENCES chill_doc.stored_object (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD location_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD organizationCost NUMERIC(10, 4) DEFAULT 0.0');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD comment_comment TEXT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD comment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD comment_userId INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event ADD CONSTRAINT FK_FA320FC864D218E FOREIGN KEY (location_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_FA320FC864D218E ON chill_event_event (location_id)');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B638671F7E88B');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event_documents DROP CONSTRAINT FK_5C1B6386EE684399');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_event_event_documents');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP location_id');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP organizationCost');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP comment_comment');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP comment_date');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_event_event DROP comment_userId');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,3 +11,11 @@ count participations to this event: >-
 | 
			
		||||
        one {Un participant à l'événement}
 | 
			
		||||
        other {# participants à l'événement}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
events:
 | 
			
		||||
    and_other_count_participants: >-
 | 
			
		||||
        { count, plural,
 | 
			
		||||
            =0 {Aucun autre participant}
 | 
			
		||||
            one {et un autre participant}
 | 
			
		||||
            other {et # autres participants}
 | 
			
		||||
        }
 | 
			
		||||
@@ -26,6 +26,8 @@ Event edit: Modifier un événement
 | 
			
		||||
Edit the event: Modifier l'événement
 | 
			
		||||
The event was updated: L'événement a été modifié
 | 
			
		||||
The event was created: L'événement a été créé
 | 
			
		||||
List of events: Liste des événements
 | 
			
		||||
Any location for this event: Aucune localisation pour cet événement
 | 
			
		||||
 | 
			
		||||
#crud participation
 | 
			
		||||
Edit all the participations: Modifier toutes les participations
 | 
			
		||||
@@ -50,6 +52,7 @@ Remove participation: Supprimer la participation
 | 
			
		||||
Delete event: Supprimer l'événement
 | 
			
		||||
Are you sure you want to remove that participation ?: Êtes-vous certain de vouloir supprimer cette participation ?
 | 
			
		||||
Are you sure you want to remove that event ?: Êtes-vous certain de vouloir supprimer cet événement, ainsi que toutes les participations associées ?
 | 
			
		||||
Any participation for this person: Cet usager ne participe à aucun évenements
 | 
			
		||||
 | 
			
		||||
#search
 | 
			
		||||
Event search: Recherche d'événements
 | 
			
		||||
@@ -107,3 +110,17 @@ csv: csv
 | 
			
		||||
Create a new role: Créer un nouveau rôle
 | 
			
		||||
Create a new type: Créer un nouveau type
 | 
			
		||||
Create a new status: Créer un nouveau statut
 | 
			
		||||
 | 
			
		||||
event:
 | 
			
		||||
    fields:
 | 
			
		||||
        organizationCost: Coût d'organisation
 | 
			
		||||
        location: Localisation
 | 
			
		||||
        documents: Documents
 | 
			
		||||
    form:
 | 
			
		||||
        organisationCost_help: Coût d'organisation pour la structure. Utile pour les statistiques.
 | 
			
		||||
        add_document: Ajouter un document
 | 
			
		||||
        remove_document: Supprimer le document
 | 
			
		||||
    filter:
 | 
			
		||||
        event_types: Par types d'événement
 | 
			
		||||
        event_dates: Par date d'événement
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
event:
 | 
			
		||||
    validation:
 | 
			
		||||
        person_already_participate_to_event: L'usager est déjà inscrit à l'événement
 | 
			
		||||
@@ -15,10 +15,14 @@ use Chill\MainBundle\CRUD\Resolver\Resolver;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorInterface;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
 | 
			
		||||
use Doctrine\DBAL\LockMode;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\OptimisticLockException;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 | 
			
		||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
@@ -173,6 +177,21 @@ abstract class AbstractCRUDController extends AbstractController
 | 
			
		||||
        if (null === $e) {
 | 
			
		||||
            throw $this->createNotFoundException(sprintf('The object %s for id %s is not found', $this->getEntityClass(), $id));
 | 
			
		||||
        }
 | 
			
		||||
        if ($request->query->has('entity_version')) {
 | 
			
		||||
            $expectedVersion = $request->query->getInt('entity_version');
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                $manager = $this->getDoctrine()->getManagerForClass($this->getEntityClass());
 | 
			
		||||
 | 
			
		||||
                if ($manager instanceof EntityManagerInterface) {
 | 
			
		||||
                    $manager->lock($e, LockMode::OPTIMISTIC, $expectedVersion);
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw new \LogicException('This manager does not allow locking.');
 | 
			
		||||
                }
 | 
			
		||||
            } catch (OptimisticLockException $e) {
 | 
			
		||||
                throw new ConflictHttpException('Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again', $e);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $e;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -135,7 +135,7 @@ class ApiController extends AbstractCRUDController
 | 
			
		||||
        try {
 | 
			
		||||
            $entity = $this->deserialize($action, $request, $_format, $entity);
 | 
			
		||||
        } catch (NotEncodableValueException $e) {
 | 
			
		||||
            throw new BadRequestHttpException('invalid json', 400, $e);
 | 
			
		||||
            throw new BadRequestHttpException('invalid json', $e, 400);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $errors = $this->validate($action, $request, $_format, $entity);
 | 
			
		||||
@@ -153,7 +153,7 @@ class ApiController extends AbstractCRUDController
 | 
			
		||||
            return $response;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->getDoctrine()->getManager()->flush();
 | 
			
		||||
        $this->getDoctrine()->getManagerForClass($this->getEntityClass())->flush();
 | 
			
		||||
 | 
			
		||||
        $response = $this->onAfterFlush($action, $request, $_format, $entity, $errors);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,12 +47,4 @@ class AdminController extends AbstractController
 | 
			
		||||
    {
 | 
			
		||||
        return $this->render('@ChillMain/Admin/indexUser.html.twig');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin")
 | 
			
		||||
     */
 | 
			
		||||
    public function indexDashboardAction()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->render('@ChillMain/Admin/indexDashboard.html.twig');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
 | 
			
		||||
class DashboardApiController
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Get user dashboard config (not yet based on user id and still hardcoded for now).
 | 
			
		||||
     *
 | 
			
		||||
     * @Route("/api/1.0/main/dashboard-config-item.json", methods={"get"})
 | 
			
		||||
     */
 | 
			
		||||
    public function getDashboardConfiguration(): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        $data = [
 | 
			
		||||
            [
 | 
			
		||||
                'position' => 'top-left',
 | 
			
		||||
                'id' => 1,
 | 
			
		||||
                'type' => 'news',
 | 
			
		||||
                'metadata' => [
 | 
			
		||||
                    // arbitrary data that will be store "some time"
 | 
			
		||||
                    'only_unread' => false,
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse($data, JsonResponse::HTTP_OK, []);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Repository\NewsItemRepository;
 | 
			
		||||
use Chill\MainBundle\Serializer\Model\Collection;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
 | 
			
		||||
class NewsItemApiController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly NewsItemRepository $newsItemRepository,
 | 
			
		||||
        private readonly SerializerInterface $serializer,
 | 
			
		||||
        private readonly PaginatorFactory $paginatorFactory
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get list of news items filtered on start and end date.
 | 
			
		||||
     *
 | 
			
		||||
     * @Route("/api/1.0/main/news/current.json", methods={"get"})
 | 
			
		||||
     */
 | 
			
		||||
    public function listCurrentNewsItems(): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        $total = $this->newsItemRepository->countCurrentNews();
 | 
			
		||||
        $paginator = $this->paginatorFactory->create($total);
 | 
			
		||||
        $newsItems = $this->newsItemRepository->findCurrentNews(
 | 
			
		||||
            $paginator->getItemsPerPage(),
 | 
			
		||||
            $paginator->getCurrentPage()->getFirstItemNumber()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse($this->serializer->serialize(
 | 
			
		||||
            new Collection(array_values($newsItems), $paginator),
 | 
			
		||||
            'json',
 | 
			
		||||
            [
 | 
			
		||||
                AbstractNormalizer::GROUPS => ['read'],
 | 
			
		||||
            ]
 | 
			
		||||
        ), JsonResponse::HTTP_OK, [], true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
class NewsItemController extends CRUDController
 | 
			
		||||
{
 | 
			
		||||
    protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
 | 
			
		||||
    {
 | 
			
		||||
        $query->addOrderBy('e.startDate', 'DESC');
 | 
			
		||||
        $query->addOrderBy('e.id', 'DESC');
 | 
			
		||||
 | 
			
		||||
        return parent::orderQuery($action, $query, $request, $paginator);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\NewsItem;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Repository\NewsItemRepository;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
 | 
			
		||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
final readonly class NewsItemHistoryController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private readonly NewsItemRepository $newsItemRepository,
 | 
			
		||||
        private readonly PaginatorFactory $paginatorFactory,
 | 
			
		||||
        private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
 | 
			
		||||
        private readonly Environment $environment,
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/{_locale}/news-items/history", name="chill_main_news_items_history")
 | 
			
		||||
     */
 | 
			
		||||
    public function list(): Response
 | 
			
		||||
    {
 | 
			
		||||
        $filter = $this->buildFilterOrder();
 | 
			
		||||
        $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString());
 | 
			
		||||
        $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString());
 | 
			
		||||
 | 
			
		||||
        $pagination = $this->paginatorFactory->create($total);
 | 
			
		||||
 | 
			
		||||
        return new Response($this->environment->render('@ChillMain/NewsItem/news_items_history.html.twig', [
 | 
			
		||||
            'entities' => $newsItems,
 | 
			
		||||
            'paginator' => $pagination,
 | 
			
		||||
            'filter_order' => $filter,
 | 
			
		||||
        ]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item")
 | 
			
		||||
     */
 | 
			
		||||
    public function showSingleItem(NewsItem $newsItem, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        return new Response($this->environment->render(
 | 
			
		||||
            '@ChillMain/NewsItem/show.html.twig',
 | 
			
		||||
            [
 | 
			
		||||
                'entity' => $newsItem,
 | 
			
		||||
            ]
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildFilterOrder(): FilterOrderHelper
 | 
			
		||||
    {
 | 
			
		||||
        $filterBuilder = $this->filterOrderHelperFactory
 | 
			
		||||
            ->create(self::class)
 | 
			
		||||
            ->addSearchBox();
 | 
			
		||||
 | 
			
		||||
        return $filterBuilder->build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,7 +19,6 @@ use Chill\MainBundle\Controller\CountryController;
 | 
			
		||||
use Chill\MainBundle\Controller\LanguageController;
 | 
			
		||||
use Chill\MainBundle\Controller\LocationController;
 | 
			
		||||
use Chill\MainBundle\Controller\LocationTypeController;
 | 
			
		||||
use Chill\MainBundle\Controller\NewsItemController;
 | 
			
		||||
use Chill\MainBundle\Controller\RegroupmentController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserJobApiController;
 | 
			
		||||
@@ -54,7 +53,6 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer;
 | 
			
		||||
use Chill\MainBundle\Entity\Language;
 | 
			
		||||
use Chill\MainBundle\Entity\Location;
 | 
			
		||||
use Chill\MainBundle\Entity\LocationType;
 | 
			
		||||
use Chill\MainBundle\Entity\NewsItem;
 | 
			
		||||
use Chill\MainBundle\Entity\Regroupment;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserJob;
 | 
			
		||||
@@ -64,7 +62,6 @@ use Chill\MainBundle\Form\CountryType;
 | 
			
		||||
use Chill\MainBundle\Form\LanguageType;
 | 
			
		||||
use Chill\MainBundle\Form\LocationFormType;
 | 
			
		||||
use Chill\MainBundle\Form\LocationTypeType;
 | 
			
		||||
use Chill\MainBundle\Form\NewsItemType;
 | 
			
		||||
use Chill\MainBundle\Form\RegroupmentType;
 | 
			
		||||
use Chill\MainBundle\Form\UserJobType;
 | 
			
		||||
use Chill\MainBundle\Form\UserType;
 | 
			
		||||
@@ -547,35 +544,6 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    'class' => NewsItem::class,
 | 
			
		||||
                    'name' => 'news_item',
 | 
			
		||||
                    'base_path' => '/admin/news_item',
 | 
			
		||||
                    'form_class' => NewsItemType::class,
 | 
			
		||||
                    'controller' => NewsItemController::class,
 | 
			
		||||
                    'actions' => [
 | 
			
		||||
                        'index' => [
 | 
			
		||||
                            'role' => 'ROLE_ADMIN',
 | 
			
		||||
                            'template' => '@ChillMain/NewsItem/index.html.twig',
 | 
			
		||||
                        ],
 | 
			
		||||
                        'new' => [
 | 
			
		||||
                            'role' => 'ROLE_ADMIN',
 | 
			
		||||
                            'template' => '@ChillMain/NewsItem/new.html.twig',
 | 
			
		||||
                        ],
 | 
			
		||||
                        'view' => [
 | 
			
		||||
                            'role' => 'ROLE_ADMIN',
 | 
			
		||||
                            'template' => '@ChillMain/NewsItem/view_admin.html.twig',
 | 
			
		||||
                        ],
 | 
			
		||||
                        'edit' => [
 | 
			
		||||
                            'role' => 'ROLE_ADMIN',
 | 
			
		||||
                            'template' => '@ChillMain/NewsItem/edit.html.twig',
 | 
			
		||||
                        ],
 | 
			
		||||
                        'delete' => [
 | 
			
		||||
                            'role' => 'ROLE_ADMIN',
 | 
			
		||||
                            'template' => '@ChillMain/NewsItem/delete.html.twig',
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
            'apis' => [
 | 
			
		||||
                [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,112 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @ORM\Entity
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\Table(name="chill_main_dashboard_config_item")
 | 
			
		||||
 */
 | 
			
		||||
class DashboardConfigItem
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Id
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\GeneratedValue
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\Column(type="integer")
 | 
			
		||||
     *
 | 
			
		||||
     * @Serializer\Groups({"dashboardConfigItem:read", "read"})
 | 
			
		||||
     */
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="string")
 | 
			
		||||
     *
 | 
			
		||||
     * @Serializer\Groups({"dashboardConfigItem:read", "read"})
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull
 | 
			
		||||
     */
 | 
			
		||||
    private string $type = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="string")
 | 
			
		||||
     *
 | 
			
		||||
     * @Serializer\Groups({"dashboardConfigItem:read", "read"})
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull
 | 
			
		||||
     */
 | 
			
		||||
    private string $position = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\ManyToOne(targetEntity=User::class)
 | 
			
		||||
     */
 | 
			
		||||
    private ?User $user = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="json", options={"default": "[]", "jsonb": true})
 | 
			
		||||
     *
 | 
			
		||||
     * @Serializer\Groups({"dashboardConfigItem:read"})
 | 
			
		||||
     */
 | 
			
		||||
    private array $metadata = [];
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getType(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setType(string $type): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->type = $type;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getPosition(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->position;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setPosition(string $position): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->position = $position;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUser(): User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setUser(User $user): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->user = $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMetadata(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->metadata;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setMetadata(array $metadata): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->metadata = $metadata;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,128 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @ORM\Entity
 | 
			
		||||
 *
 | 
			
		||||
 * @ORM\Table(name="chill_main_news")
 | 
			
		||||
 */
 | 
			
		||||
class NewsItem implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Id
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\GeneratedValue
 | 
			
		||||
     *
 | 
			
		||||
     * @ORM\Column(type="integer")
 | 
			
		||||
     *
 | 
			
		||||
     * @Groups({"read"})
 | 
			
		||||
     */
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="text")
 | 
			
		||||
     *
 | 
			
		||||
     * @Groups({"read"})
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotBlank
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull
 | 
			
		||||
     */
 | 
			
		||||
    private string $title = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="text")
 | 
			
		||||
     *
 | 
			
		||||
     * @Groups({"read"})
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotBlank
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull
 | 
			
		||||
     */
 | 
			
		||||
    private string $content = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="date_immutable", nullable=false)
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\NotNull
 | 
			
		||||
     *
 | 
			
		||||
     * @Groups({"read"})
 | 
			
		||||
     */
 | 
			
		||||
    private ?\DateTimeImmutable $startDate = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
 | 
			
		||||
     *
 | 
			
		||||
     * @Assert\GreaterThanOrEqual(propertyPath="startDate")
 | 
			
		||||
     *
 | 
			
		||||
     * @Groups({"read"})
 | 
			
		||||
     */
 | 
			
		||||
    private ?\DateTimeImmutable $endDate = null;
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setTitle(string $title): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->title = $title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContent(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setContent(string $content): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->content = $content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setStartDate(?\DateTimeImmutable $startDate): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->startDate = $startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setEndDate(?\DateTimeImmutable $endDate): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->endDate = $endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Form;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\NewsItem;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillDateType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
use Symfony\Component\OptionsResolver\OptionsResolver;
 | 
			
		||||
 | 
			
		||||
class NewsItemType extends AbstractType
 | 
			
		||||
{
 | 
			
		||||
    public function buildForm(FormBuilderInterface $builder, array $options)
 | 
			
		||||
    {
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('title', TextType::class, [
 | 
			
		||||
                'required' => true,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add('content', ChillTextareaType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
            ])
 | 
			
		||||
            ->add(
 | 
			
		||||
                'startDate',
 | 
			
		||||
                ChillDateType::class,
 | 
			
		||||
                [
 | 
			
		||||
                    'required' => true,
 | 
			
		||||
                    'input' => 'datetime_immutable',
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
            ->add('endDate', ChillDateType::class, [
 | 
			
		||||
                'required' => false,
 | 
			
		||||
                'input' => 'datetime_immutable',
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function configureOptions(OptionsResolver $resolver)
 | 
			
		||||
    {
 | 
			
		||||
        $resolver->setDefault('data_class', NewsItem::class);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Form\Type;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Entity\PermissionsGroup;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
@@ -27,7 +28,13 @@ class ComposedGroupCenterType extends AbstractType
 | 
			
		||||
            'choice_label' => static fn (PermissionsGroup $group) => $group->getName(),
 | 
			
		||||
        ])->add('center', EntityType::class, [
 | 
			
		||||
            'class' => Center::class,
 | 
			
		||||
            'choice_label' => static fn (Center $center) => $center->getName(),
 | 
			
		||||
            'query_builder' => static function (EntityRepository $er) {
 | 
			
		||||
                $qb = $er->createQueryBuilder('c');
 | 
			
		||||
                $qb->where($qb->expr()->eq('c.isActive', 'TRUE'))
 | 
			
		||||
                    ->orderBy('c.name', 'ASC');
 | 
			
		||||
 | 
			
		||||
                return $qb;
 | 
			
		||||
            },
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,8 @@ class PickUserDynamicType extends AbstractType
 | 
			
		||||
        $view->vars['types'] = ['user'];
 | 
			
		||||
        $view->vars['uniqid'] = uniqid('pick_user_dyn');
 | 
			
		||||
        $view->vars['suggested'] = [];
 | 
			
		||||
        $view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
 | 
			
		||||
        $view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
 | 
			
		||||
 | 
			
		||||
        foreach ($options['suggested'] as $user) {
 | 
			
		||||
            $view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
 | 
			
		||||
@@ -54,7 +56,12 @@ class PickUserDynamicType extends AbstractType
 | 
			
		||||
            ->setDefault('multiple', false)
 | 
			
		||||
            ->setAllowedTypes('multiple', ['bool'])
 | 
			
		||||
            ->setDefault('compound', false)
 | 
			
		||||
            ->setDefault('suggested', []);
 | 
			
		||||
            ->setDefault('suggested', [])
 | 
			
		||||
            // if set to true, only the id will be set inside the content. The denormalization will not work.
 | 
			
		||||
            ->setDefault('as_id', false)
 | 
			
		||||
            ->setAllowedTypes('as_id', ['bool'])
 | 
			
		||||
            ->setDefault('submit_on_adding_new_entity', false)
 | 
			
		||||
            ->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getBlockPrefix()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ use Symfony\Component\Routing\RouterInterface;
 | 
			
		||||
/**
 | 
			
		||||
 * Create paginator instances.
 | 
			
		||||
 */
 | 
			
		||||
class PaginatorFactory
 | 
			
		||||
final readonly class PaginatorFactory implements PaginatorFactoryInterface
 | 
			
		||||
{
 | 
			
		||||
    final public const DEFAULT_CURRENT_PAGE_KEY = 'page';
 | 
			
		||||
 | 
			
		||||
@@ -25,23 +25,20 @@ class PaginatorFactory
 | 
			
		||||
 | 
			
		||||
    final public const DEFAULT_PAGE_NUMBER = 1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param int $itemPerPage
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        /**
 | 
			
		||||
         * the request stack.
 | 
			
		||||
         */
 | 
			
		||||
        private readonly RequestStack $requestStack,
 | 
			
		||||
        private RequestStack $requestStack,
 | 
			
		||||
        /**
 | 
			
		||||
         * the router and generator for url.
 | 
			
		||||
         */
 | 
			
		||||
        private readonly RouterInterface $router,
 | 
			
		||||
        private RouterInterface $router,
 | 
			
		||||
        /**
 | 
			
		||||
         * the default item per page. This may be overriden by
 | 
			
		||||
         * the request or inside the paginator.
 | 
			
		||||
         */
 | 
			
		||||
        private $itemPerPage = 20
 | 
			
		||||
        private int $itemPerPage = 20
 | 
			
		||||
    ) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -51,17 +48,14 @@ class PaginatorFactory
 | 
			
		||||
     * The default route and route parameters are the current ones. If set,
 | 
			
		||||
     * thos route are overriden.
 | 
			
		||||
     *
 | 
			
		||||
     * @param int         $totalItems
 | 
			
		||||
     * @param string|null $route           the specific route to use in pages
 | 
			
		||||
     * @param array|null  $routeParameters the specific route parameters to use in pages
 | 
			
		||||
     *
 | 
			
		||||
     * @return PaginatorInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function create(
 | 
			
		||||
        $totalItems,
 | 
			
		||||
        int $totalItems,
 | 
			
		||||
        ?string $route = null,
 | 
			
		||||
        ?array $routeParameters = null
 | 
			
		||||
    ) {
 | 
			
		||||
    ): PaginatorInterface {
 | 
			
		||||
        return new Paginator(
 | 
			
		||||
            $totalItems,
 | 
			
		||||
            $this->getCurrentItemsPerPage(),
 | 
			
		||||
@@ -74,7 +68,7 @@ class PaginatorFactory
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCurrentItemsPerPage()
 | 
			
		||||
    public function getCurrentItemsPerPage(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->requestStack
 | 
			
		||||
            ->getCurrentRequest()
 | 
			
		||||
@@ -82,16 +76,13 @@ class PaginatorFactory
 | 
			
		||||
            ->getInt(self::DEFAULT_ITEM_PER_NUMBER_KEY, $this->itemPerPage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCurrentPageFirstItemNumber()
 | 
			
		||||
    public function getCurrentPageFirstItemNumber(): int
 | 
			
		||||
    {
 | 
			
		||||
        return ($this->getCurrentPageNumber() - 1) *
 | 
			
		||||
            $this->getCurrentItemsPerPage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return int
 | 
			
		||||
     */
 | 
			
		||||
    public function getCurrentPageNumber()
 | 
			
		||||
    public function getCurrentPageNumber(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->requestStack
 | 
			
		||||
            ->getCurrentRequest()
 | 
			
		||||
@@ -99,14 +90,14 @@ class PaginatorFactory
 | 
			
		||||
            ->getInt(self::DEFAULT_CURRENT_PAGE_KEY, self::DEFAULT_PAGE_NUMBER);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getCurrentRoute()
 | 
			
		||||
    private function getCurrentRoute()
 | 
			
		||||
    {
 | 
			
		||||
        $request = $this->requestStack->getCurrentRequest();
 | 
			
		||||
 | 
			
		||||
        return $request->get('_route');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getCurrentRouteParameters()
 | 
			
		||||
    private function getCurrentRouteParameters()
 | 
			
		||||
    {
 | 
			
		||||
        return array_merge(
 | 
			
		||||
            $this->router->getContext()->getParameters(),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Pagination;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create paginator instances.
 | 
			
		||||
 */
 | 
			
		||||
interface PaginatorFactoryInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * create a paginator instance.
 | 
			
		||||
     *
 | 
			
		||||
     * The default route and route parameters are the current ones. If set,
 | 
			
		||||
     * thos route are overriden.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $route           the specific route to use in pages
 | 
			
		||||
     * @param array|null  $routeParameters the specific route parameters to use in pages
 | 
			
		||||
     */
 | 
			
		||||
    public function create(int $totalItems, ?string $route = null, ?array $routeParameters = null): PaginatorInterface;
 | 
			
		||||
 | 
			
		||||
    public function getCurrentItemsPerPage(): int;
 | 
			
		||||
 | 
			
		||||
    public function getCurrentPageFirstItemNumber(): int;
 | 
			
		||||
 | 
			
		||||
    public function getCurrentPageNumber(): int;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\NewsItem;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
 | 
			
		||||
class NewsItemRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private readonly EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $entityManager, private readonly ClockInterface $clock)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $entityManager->getRepository(NewsItem::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->createQueryBuilder($alias, $indexBy);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    {
 | 
			
		||||
        return NewsItem::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildBaseQuery(
 | 
			
		||||
        ?string $pattern = null
 | 
			
		||||
    ): QueryBuilder {
 | 
			
		||||
        $qb = $this->createQueryBuilder('n');
 | 
			
		||||
 | 
			
		||||
        $qb->where('n.startDate <= :now');
 | 
			
		||||
        $qb->setParameter('now', $this->clock->now());
 | 
			
		||||
 | 
			
		||||
        if (null !== $pattern && '' !== $pattern) {
 | 
			
		||||
            $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))'))
 | 
			
		||||
                ->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))'))
 | 
			
		||||
                ->setParameter('pattern', '%'.$pattern.'%');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAllFilteredBySearchTerm(?string $pattern = null)
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->buildBaseQuery($pattern);
 | 
			
		||||
        $qb
 | 
			
		||||
            ->addOrderBy('n.startDate', 'DESC')
 | 
			
		||||
            ->addOrderBy('n.id', 'DESC');
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<NewsItem>
 | 
			
		||||
     */
 | 
			
		||||
    public function findCurrentNews(?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->buildQueryCurrentNews();
 | 
			
		||||
 | 
			
		||||
        if (null !== $limit) {
 | 
			
		||||
            $qb->setMaxResults($limit);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $offset) {
 | 
			
		||||
            $qb->setFirstResult($offset);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $qb
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
            ->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function countAllFilteredBySearchTerm(?string $pattern = null)
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->buildBaseQuery($pattern);
 | 
			
		||||
 | 
			
		||||
        return $qb
 | 
			
		||||
            ->select('COUNT(n)')
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
            ->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function countCurrentNews()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->buildQueryCurrentNews()
 | 
			
		||||
            ->select('COUNT(n)')
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
            ->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildQueryCurrentNews(): QueryBuilder
 | 
			
		||||
    {
 | 
			
		||||
        $now = $this->clock->now();
 | 
			
		||||
 | 
			
		||||
        $qb = $this->createQueryBuilder('n');
 | 
			
		||||
        $qb
 | 
			
		||||
            ->where(
 | 
			
		||||
                $qb->expr()->andX(
 | 
			
		||||
                    $qb->expr()->lte('n.startDate', ':now'),
 | 
			
		||||
                    $qb->expr()->orX(
 | 
			
		||||
                        $qb->expr()->gt('n.endDate', ':now'),
 | 
			
		||||
                        $qb->expr()->isNull('n.endDate')
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            ->setParameter('now', $now);
 | 
			
		||||
 | 
			
		||||
        return $qb;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -55,11 +55,20 @@ export interface ServerExceptionInterface extends TransportExceptionInterface {
 | 
			
		||||
  body: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ConflictHttpExceptionInterface extends TransportExceptionInterface {
 | 
			
		||||
    name: 'ConflictHttpException';
 | 
			
		||||
    violations: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic api method that can be adapted to any fetch request
 | 
			
		||||
 */
 | 
			
		||||
export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => {
 | 
			
		||||
export const makeFetch = <Input, Output>(
 | 
			
		||||
    method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE',
 | 
			
		||||
    url: string, body?: body | Input | null,
 | 
			
		||||
    options?: FetchParams
 | 
			
		||||
): Promise<Output> => {
 | 
			
		||||
 | 
			
		||||
    let opts = {
 | 
			
		||||
        method: method,
 | 
			
		||||
        headers: {
 | 
			
		||||
@@ -67,6 +76,7 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  if (body !== null && typeof body !== 'undefined') {
 | 
			
		||||
      Object.assign(opts, {body: JSON.stringify(body)})
 | 
			
		||||
    }
 | 
			
		||||
@@ -90,6 +100,10 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
 | 
			
		||||
            throw AccessException(response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (response.status === 409) {
 | 
			
		||||
            throw ConflictHttpException(response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw {
 | 
			
		||||
            name: 'Exception',
 | 
			
		||||
            sta: response.status,
 | 
			
		||||
@@ -220,3 +234,12 @@ const ServerException = (code: number, body: string): ServerExceptionInterface =
 | 
			
		||||
 | 
			
		||||
  return error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ConflictHttpException = (response: Response): ConflictHttpExceptionInterface => {
 | 
			
		||||
    const error = {} as ConflictHttpExceptionInterface;
 | 
			
		||||
 | 
			
		||||
    error.name = 'ConflictHttpException';
 | 
			
		||||
    error.violations = ['Sorry, but someone else has already changed this entity. Please refresh the page and apply the changes again']
 | 
			
		||||
 | 
			
		||||
    return error;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,14 @@ import AddressDetailsButton from "../../vuejs/_components/AddressDetails/Address
 | 
			
		||||
import {createApp} from "vue";
 | 
			
		||||
import {createI18n} from "vue-i18n";
 | 
			
		||||
import {_createI18n} from "../../vuejs/_js/i18n";
 | 
			
		||||
import {Address} from "../../types";
 | 
			
		||||
import {Address, AddressRefStatus} from "../../types";
 | 
			
		||||
 | 
			
		||||
const i18n = _createI18n({});
 | 
			
		||||
 | 
			
		||||
document.querySelectorAll<HTMLSpanElement>('span[data-address-details]').forEach((el) => {
 | 
			
		||||
  const dataset = el.dataset as {
 | 
			
		||||
    addressId: string,
 | 
			
		||||
    addressRefStatus: string,
 | 
			
		||||
    addressRefStatus: AddressRefStatus,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const app = createApp({
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
import './index.scss';
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
div.flex-table {
 | 
			
		||||
    .news-content {
 | 
			
		||||
        p {
 | 
			
		||||
            margin-top: 1rem;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,10 @@ function loadDynamicPicker(element) {
 | 
			
		||||
                    (input.value === '[]' || input.value === '') ?
 | 
			
		||||
                        null : [ JSON.parse(input.value) ]
 | 
			
		||||
                )
 | 
			
		||||
            suggested = JSON.parse(el.dataset.suggested)
 | 
			
		||||
            suggested = JSON.parse(el.dataset.suggested),
 | 
			
		||||
            as_id = parseInt(el.dataset.asId) === 1,
 | 
			
		||||
            submit_on_adding_new_entity = parseInt(el.dataset.submitOnAddingNewEntity) === 1
 | 
			
		||||
            label = el.dataset.label;
 | 
			
		||||
 | 
			
		||||
        if (!isMultiple) {
 | 
			
		||||
            if (input.value === '[]'){
 | 
			
		||||
@@ -39,6 +42,7 @@ function loadDynamicPicker(element) {
 | 
			
		||||
                ':picked="picked" ' +
 | 
			
		||||
                ':uniqid="uniqid" ' +
 | 
			
		||||
                ':suggested="notPickedSuggested" ' +
 | 
			
		||||
                ':label="label" ' +
 | 
			
		||||
                '@addNewEntity="addNewEntity" ' +
 | 
			
		||||
                '@removeEntity="removeEntity"></pick-entity>',
 | 
			
		||||
            components: {
 | 
			
		||||
@@ -50,7 +54,10 @@ function loadDynamicPicker(element) {
 | 
			
		||||
                    types: JSON.parse(el.dataset.types),
 | 
			
		||||
                    picked: picked === null ? [] : picked,
 | 
			
		||||
                    uniqid: el.dataset.uniqid,
 | 
			
		||||
                    suggested: suggested
 | 
			
		||||
                    suggested,
 | 
			
		||||
                    as_id,
 | 
			
		||||
                    submit_on_adding_new_entity,
 | 
			
		||||
                    label,
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            computed: {
 | 
			
		||||
@@ -69,7 +76,12 @@ function loadDynamicPicker(element) {
 | 
			
		||||
                            return el.type === entity.type && el.id === entity.id;
 | 
			
		||||
                        })) {
 | 
			
		||||
                            this.picked.push(entity);
 | 
			
		||||
                            input.value = JSON.stringify(this.picked);
 | 
			
		||||
                            if (!as_id) {
 | 
			
		||||
                              input.value = JSON.stringify(this.picked);
 | 
			
		||||
                            } else {
 | 
			
		||||
                              const ids = this.picked.map(el => el.id);
 | 
			
		||||
                              input.value = ids.join(',');
 | 
			
		||||
                            }
 | 
			
		||||
                            console.log(entity)
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
@@ -78,9 +90,17 @@ function loadDynamicPicker(element) {
 | 
			
		||||
                        })) {
 | 
			
		||||
                            this.picked.splice(0, this.picked.length);
 | 
			
		||||
                            this.picked.push(entity);
 | 
			
		||||
                            input.value = JSON.stringify(this.picked[0]);
 | 
			
		||||
                            if (!as_id) {
 | 
			
		||||
                              input.value = JSON.stringify(this.picked[0]);
 | 
			
		||||
                            } else {
 | 
			
		||||
                              input.value = this.picked.map(el => el.id);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (this.submit_on_adding_new_entity) {
 | 
			
		||||
                        input.form.submit();
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                removeEntity({entity}) {
 | 
			
		||||
                    if (-1 === this.suggested.findIndex(e => e.type === entity.type && e.id === entity.id)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -160,11 +160,3 @@ export interface LocationType {
 | 
			
		||||
  contactData: "optional" | "required";
 | 
			
		||||
  title: TranslatableString;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NewsItemType {
 | 
			
		||||
    id: number;
 | 
			
		||||
    title: string;
 | 
			
		||||
    content: string;
 | 
			
		||||
    startDate: DateTime;
 | 
			
		||||
    endDate: DateTime | null;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -97,8 +97,6 @@ import MyNotifications from './MyNotifications';
 | 
			
		||||
import MyWorkflows from './MyWorkflows.vue';
 | 
			
		||||
import TabCounter from './TabCounter';
 | 
			
		||||
import { mapState } from "vuex";
 | 
			
		||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
   name: "App",
 | 
			
		||||
@@ -114,7 +112,7 @@ export default {
 | 
			
		||||
   },
 | 
			
		||||
   data() {
 | 
			
		||||
      return {
 | 
			
		||||
         activeTab: 'MyCustoms',
 | 
			
		||||
         activeTab: 'MyCustoms'
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   computed: {
 | 
			
		||||
@@ -128,11 +126,8 @@ export default {
 | 
			
		||||
   },
 | 
			
		||||
   methods: {
 | 
			
		||||
      selectTab(tab) {
 | 
			
		||||
          if (tab !== 'MyCustoms') {
 | 
			
		||||
              this.$store.dispatch('getByTab', { tab: tab });
 | 
			
		||||
          }
 | 
			
		||||
         this.$store.dispatch('getByTab', { tab: tab });
 | 
			
		||||
         this.activeTab = tab;
 | 
			
		||||
          console.log(this.activeTab)
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   mounted() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div v-if="newsItems.length > 0">
 | 
			
		||||
        <h1>{{ $t('widget.news.title') }}</h1>
 | 
			
		||||
        <ul class="scrollable">
 | 
			
		||||
            <NewsItem v-for="item in newsItems" :item="item" :key="item.id" />
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { onMounted, ref } from 'vue'
 | 
			
		||||
import { fetchResults } from '../../../lib/api/apiMethods';
 | 
			
		||||
import Modal from '../../_components/Modal.vue';
 | 
			
		||||
import { NewsItemType } from '../../../types';
 | 
			
		||||
import NewsItem from './NewsItem.vue';
 | 
			
		||||
 | 
			
		||||
const newsItems = ref<NewsItemType[]>([])
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    fetchResults<NewsItemType>('/api/1.0/main/news/current.json')
 | 
			
		||||
        .then((news): Promise<void> => {
 | 
			
		||||
            // console.log('news articles', response.results)
 | 
			
		||||
            newsItems.value = news;
 | 
			
		||||
 | 
			
		||||
            return Promise.resolve();
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error: string) => {
 | 
			
		||||
            console.error('Error fetching news items', error);
 | 
			
		||||
        })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
ul {
 | 
			
		||||
    list-style: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1 {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,183 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <li>
 | 
			
		||||
        <h2>{{ props.item.title }}</h2>
 | 
			
		||||
        <time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
 | 
			
		||||
        <div class="content" v-if="shouldTruncate(item.content)">
 | 
			
		||||
            <div v-html="prepareContent(item.content)"></div>
 | 
			
		||||
            <div class="float-end">
 | 
			
		||||
                <button class="btn btn-sm btn-show read-more" @click="() => openModal(item)">{{ $t('widget.news.readMore') }}</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="content" v-else>
 | 
			
		||||
            <div v-html="convertMarkdownToHtml(item.content)"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <modal v-if="showModal" @close="closeModal">
 | 
			
		||||
            <template #header>
 | 
			
		||||
                <p class="news-title">{{ item.title }}</p>
 | 
			
		||||
            </template>
 | 
			
		||||
            <template #body>
 | 
			
		||||
                <p class="news-date">
 | 
			
		||||
                    <time class="createdBy" datetime="{{item.startDate.datetime}}">{{ $d(newsItemStartDate(), 'text') }}</time>
 | 
			
		||||
                </p>
 | 
			
		||||
                <div v-html="convertMarkdownToHtml(item.content)"></div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </modal>
 | 
			
		||||
    </li>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
import DOMPurify from 'dompurify';
 | 
			
		||||
import { DateTime, NewsItemType } from "../../../types";
 | 
			
		||||
import type { PropType } from 'vue'
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import {ISOToDatetime} from '../../../chill/js/date';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    item: {
 | 
			
		||||
        type: Object as PropType<NewsItemType>,
 | 
			
		||||
        required: true
 | 
			
		||||
    },
 | 
			
		||||
    maxLength: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: 350,
 | 
			
		||||
    },
 | 
			
		||||
    maxLines: {
 | 
			
		||||
        type: Number,
 | 
			
		||||
        required: false,
 | 
			
		||||
        default: 3
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const selectedArticle = ref<NewsItemType | null>(null);
 | 
			
		||||
const showModal = ref(false);
 | 
			
		||||
 | 
			
		||||
const openModal = (item: NewsItemType) => {
 | 
			
		||||
    selectedArticle.value = item;
 | 
			
		||||
    showModal.value = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeModal = () => {
 | 
			
		||||
    selectedArticle.value = null;
 | 
			
		||||
    showModal.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const shouldTruncate = (content: string): boolean => {
 | 
			
		||||
    const lines = content.split('\n');
 | 
			
		||||
 | 
			
		||||
    // Check if any line exceeds the maximum length
 | 
			
		||||
    const tooManyLines = lines.length > props.maxLines;
 | 
			
		||||
 | 
			
		||||
    return content.length > props.maxLength || tooManyLines;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const truncateContent = (content: string): string => {
 | 
			
		||||
    let truncatedContent = content.slice(0, props.maxLength);
 | 
			
		||||
    let linkDepth = 0;
 | 
			
		||||
    let linkStartIndex = -1;
 | 
			
		||||
    const lines = content.split('\n');
 | 
			
		||||
 | 
			
		||||
    // Truncate if amount of lines are too many
 | 
			
		||||
    if (lines.length > props.maxLines && content.length < props.maxLength) {
 | 
			
		||||
        const truncatedContent = lines.slice(0, props.maxLines).join('\n').trim();
 | 
			
		||||
        return truncatedContent + '...';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < truncatedContent.length; i++) {
 | 
			
		||||
        const char = truncatedContent[i];
 | 
			
		||||
 | 
			
		||||
        if (char === '[') {
 | 
			
		||||
            linkDepth++;
 | 
			
		||||
            if (linkDepth === 1) {
 | 
			
		||||
                linkStartIndex = i;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (char === ']') {
 | 
			
		||||
            linkDepth = Math.max(0, linkDepth - 1);
 | 
			
		||||
        } else if (char === '(' && linkDepth === 0) {
 | 
			
		||||
            truncatedContent = truncatedContent.slice(0, i);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (linkDepth > 0) {
 | 
			
		||||
        truncatedContent += ']';
 | 
			
		||||
        linkDepth--;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If a link was found, append the URL inside the parentheses
 | 
			
		||||
    if (linkStartIndex !== -1) {
 | 
			
		||||
        const linkEndIndex = content.indexOf(')', linkStartIndex);
 | 
			
		||||
        const url = content.slice(linkStartIndex + 1, linkEndIndex);
 | 
			
		||||
        truncatedContent = truncatedContent.slice(0, linkStartIndex) + `(${url})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    truncatedContent += '...';
 | 
			
		||||
 | 
			
		||||
    return truncatedContent;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const preprocess = (markdown: string): string => {
 | 
			
		||||
    return markdown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const postprocess = (html: string): string => {
 | 
			
		||||
    DOMPurify.addHook('afterSanitizeAttributes', (node) => {
 | 
			
		||||
        if ('target' in node) {
 | 
			
		||||
            node.setAttribute('target', '_blank');
 | 
			
		||||
            node.setAttribute('rel', 'noopener noreferrer');
 | 
			
		||||
        }
 | 
			
		||||
        if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
 | 
			
		||||
            node.setAttribute('xlink:show', 'new');
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    return DOMPurify.sanitize(html);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const convertMarkdownToHtml = (markdown: string): string => {
 | 
			
		||||
    marked.use({'hooks': {postprocess, preprocess}});
 | 
			
		||||
    const rawHtml = marked(markdown);
 | 
			
		||||
    return rawHtml;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const prepareContent = (content: string): string => {
 | 
			
		||||
    const htmlContent = convertMarkdownToHtml(content);
 | 
			
		||||
    return truncateContent(htmlContent);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const newsItemStartDate = (): null|Date => {
 | 
			
		||||
    return ISOToDatetime(props.item?.startDate.datetime);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
li {
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: .8rem;
 | 
			
		||||
    background-color: #fbfbfb;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h2 {
 | 
			
		||||
    font-size: 1rem !important;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content {
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    font-size: .9rem;
 | 
			
		||||
    position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.news-title {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user