Add form and types to handle async files

This commit is contained in:
Julien Fastré 2023-12-11 18:37:58 +01:00
parent 1195767eb3
commit 28d09a8206
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
10 changed files with 271 additions and 78 deletions

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Entity;
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
use ChampsLibres\WopiLib\Contract\Entity\Document;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;

View File

@ -14,13 +14,10 @@ namespace Chill\DocStoreBundle\Form;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Entity\Document;
use Chill\DocStoreBundle\Entity\DocumentCategory;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectManager;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -29,33 +26,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class AccompanyingCourseDocumentType extends AbstractType
{
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var ObjectManager
*/
protected $om;
/**
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
/**
* the user running this form.
*
* @var User
*/
protected $user;
public function __construct(
TranslatableStringHelper $translatableStringHelper
) {
$this->translatableStringHelper = $translatableStringHelper;
}
private readonly TranslatableStringHelperInterface $translatableStringHelper
) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{

View File

@ -20,23 +20,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class DocumentCategoryType extends AbstractType
{
private $chillBundlesFlipped;
public function __construct($kernelBundles)
{
// TODO faire un service dans CHillMain
foreach ($kernelBundles as $key => $value) {
if (str_starts_with((string) $key, 'Chill')) {
$this->chillBundlesFlipped[$value] = $key;
}
}
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$bundles = [
'chill-doc-store' => 'chill-doc-store',
];
$builder
->add('bundleId', ChoiceType::class, [
'choices' => $this->chillBundlesFlipped,
'choices' => $bundles,
'disabled' => false,
])
->add('idInsideBundle', null, [
@ -44,7 +36,7 @@ class DocumentCategoryType extends AbstractType
])
->add('documentClass', null, [
'disabled' => false,
]) // cahcerh par default PersonDocument
])
->add('name', TranslatableStringFormType::class);
}

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Form;
use ChampsLibres\AsyncUploaderBundle\Form\Type\AsyncUploaderType;
use Chill\DocStoreBundle\Form\Type\AsyncUploaderType;
use Chill\DocStoreBundle\Entity\StoredObject;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
@ -26,15 +26,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class StoredObjectType extends AbstractType
{
/**
* @var EntityManagerInterface
*/
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function __construct(private readonly EntityManagerInterface $em) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{

View File

@ -0,0 +1,77 @@
<?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\Form\Type;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AsyncUploaderType extends AbstractType
{
private readonly int $expires_delay;
private readonly int $max_submit_delay;
private readonly int $max_post_file_size;
public function __construct(
private readonly UrlGeneratorInterface $url_generator,
ParameterBagInterface $parameters,
) {
$config = $parameters->get('chill_doc_store')['openstack']['temp_url'];
$this->expires_delay = $config['max_expires_delay'];
$this->max_submit_delay = $config['max_submit_delay'];
$this->max_post_file_size = $config['max_post_file_size'];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'expires_delay' => $this->expires_delay,
'max_post_size' => $this->max_post_file_size,
'submit_delay' => $this->max_submit_delay,
'max_files' => 1,
'error_bubbling' => false,
]);
$resolver->setAllowedTypes('expires_delay', ['int']);
$resolver->setAllowedTypes('max_post_size', ['int']);
$resolver->setAllowedTypes('max_files', ['int']);
$resolver->setAllowedTypes('submit_delay', ['int']);
}
public function buildView(
FormView $view,
FormInterface $form,
array $options
) {
$view->vars['attr']['data-async-file-upload'] = true;
$view->vars['attr']['data-generate-temp-url-post'] = $this
->url_generator->generate('async_upload.generate_url', [
'expires_delay' => $options['expires_delay'],
'method' => 'post',
'submit_delay' => $options['submit_delay'],
]);
$view->vars['attr']['data-temp-url-get'] = $this->url_generator
->generate('async_upload.generate_url', ['method' => 'GET']);
$view->vars['attr']['data-max-files'] = $options['max_files'];
$view->vars['attr']['data-max-post-size'] = $options['max_post_size'];
}
public function getParent()
{
return HiddenType::class;
}
}

View File

@ -0,0 +1,67 @@
<?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\Validator\Constraints;
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExists;
use Chill\DocStoreBundle\Validator\Constraints\AsyncFileExistsValidator;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @internal
*
* @coversNothing
*/
class AsyncFileExistsValidatorTest extends ConstraintValidatorTestCase
{
use ProphecyTrait;
protected function createValidator()
{
$client = new MockHttpClient(function ($method, $url, $options): MockResponse {
if (str_contains((string) $url, '404')) {
return new MockResponse('', ['http_code' => 404]);
}
return new MockResponse('', ['http_code' => 200]);
});
$generator = $this->prophesize(TempUrlGeneratorInterface::class);
$generator->generate(Argument::in(['GET', 'HEAD']), Argument::type('string'), Argument::any())
->will(fn (array $args): SignedUrl => new SignedUrl($args[0], 'https://object.store.example/container/'.$args[1], new \DateTimeImmutable('1 hours')));
return new AsyncFileExistsValidator($generator->reveal(), $client);
}
public function testWhenFileExistsIsValid(): void
{
$this->validator->validate((new StoredObject())->setFilename('present'), new AsyncFileExists());
$this->assertNoViolation();
}
public function testWhenFileIsNotPresent(): void
{
$this->validator->validate(
(new StoredObject())->setFilename('is_404'),
new AsyncFileExists(['message' => 'my_message'])
);
$this->buildViolation('my_message')->setParameter('{{ filename }}', 'is_404')->assertRaised();
}
}

View File

@ -0,0 +1,32 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
final class AsyncFileExists extends Constraint
{
public string $message = "The file '{{ filename }}' is not stored properly.";
public function validatedBy()
{
return AsyncFileExistsValidator::class;
}
public function getTargets()
{
return [Constraint::CLASS_CONSTRAINT, Constraint::PROPERTY_CONSTRAINT];
}
}

View File

@ -0,0 +1,67 @@
<?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\Validator\Constraints;
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
final class AsyncFileExistsValidator extends ConstraintValidator
{
public function __construct(
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
private readonly HttpClientInterface $client
) {}
public function validate($value, Constraint $constraint): void
{
if ($value instanceof StoredObject) {
$this->validateObject($value->getFilename(), $constraint);
} elseif (is_string($value)) {
$this->validateObject($value, $constraint);
} else {
throw new UnexpectedValueException($value, StoredObject::class.' or string');
}
}
protected function validateObject(string $file, Constraint $constraint): void
{
if (!$constraint instanceof AsyncFileExists) {
throw new UnexpectedTypeException($constraint, AsyncFileExists::class);
}
$urlHead = $this->tempUrlGenerator->generate(
'HEAD',
$file,
30
);
try {
$response = $this->client->request('HEAD', $urlHead->url);
if (404 === $response->getStatusCode()) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ filename }}', $file)
->addViolation();
}
} catch (HttpExceptionInterface $exception) {
if (404 !== $exception->getResponse()->getStatusCode()) {
throw $exception;
}
}
}
}

View File

@ -8,18 +8,6 @@ services:
tags:
- { name: doctrine.repository_service }
Chill\DocStoreBundle\Form\DocumentCategoryType:
class: Chill\DocStoreBundle\Form\DocumentCategoryType
arguments: [ "%kernel.bundles%" ]
tags:
- { name: form.type }
Chill\DocStoreBundle\Form\PersonDocumentType:
class: Chill\DocStoreBundle\Form\PersonDocumentType
# arguments:
# - "@chill.main.helper.translatable_string"
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
Chill\DocStoreBundle\Security\Authorization\:
resource: "./../Security/Authorization"
@ -54,6 +42,9 @@ services:
Chill\DocStoreBundle\GenericDoc\Renderer\:
resource: '../GenericDoc/Renderer/'
Chill\DocStoreBundle\Validator\:
resource: '../Validator'
Chill\DocStoreBundle\AsyncUpload\Driver\:
resource: '../AsyncUpload/Driver/'

View File

@ -1,13 +1,15 @@
services:
Chill\DocStoreBundle\Form\StoredObjectType:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
tags:
- { name: form.type }
_defaults:
autowire: true
autoconfigure: true
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
class: Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType
arguments:
- "@chill.main.helper.translatable_string"
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
Chill\DocStoreBundle\Form\:
resource: '../../Form'
Chill\DocStoreBundle\Form\PersonDocumentType:
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }
Chill\DocStoreBundle\Form\AccompanyingCourseDocumentType:
tags:
- { name: form.type, alias: chill_docstorebundle_form_document }