Skip to content

Commit

Permalink
Add tests for export (#1705)
Browse files Browse the repository at this point in the history
* Add tests for export

* Fix tests

* Document the hack for Symfony < 6.2

* Actualizar ProxyQuery.php

* Update src/Datagrid/ProxyQuery.php

Co-authored-by: Vincent Langlet <VincentLanglet@users.noreply.github.com>

* Add tests for setOrderBy with empty values

* Recover removed test

---------

Co-authored-by: Vincent Langlet <VincentLanglet@users.noreply.github.com>
  • Loading branch information
jordisala1991 and VincentLanglet committed Apr 15, 2023
1 parent 18d5bd5 commit 5f11ecb
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 7 deletions.
6 changes: 6 additions & 0 deletions src/Datagrid/ProxyQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ public function getDoctrineQuery(): Query

public function setSortBy(array $parentAssociationMappings, array $fieldMapping): BaseProxyQueryInterface
{
if (!isset($fieldMapping['fieldName'])) {
$this->sortBy = null;

return $this;
}

$alias = $this->entityJoin($parentAssociationMappings);
$this->sortBy = $alias.'.'.$fieldMapping['fieldName'];

Expand Down
58 changes: 56 additions & 2 deletions src/Exporter/DataSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Sonata\DoctrineORMAdminBundle\Exporter;

use Doctrine\ORM\Mapping\ClassMetadata;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface as BaseProxyQueryInterface;
use Sonata\AdminBundle\Exporter\DataSourceInterface;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface;
Expand All @@ -36,12 +37,16 @@ public function createIterator(BaseProxyQueryInterface $query, array $fields): \
$sortBy = $query->getSortBy();

// AddSelect is needed when exporting the results sorted by a column that is part of ManyToOne relation
// For OneToMany the toIterable() doctrine method is not supported so the select is not added and the sort is removed.
//
// @see https://github.com/sonata-project/SonataDoctrineORMAdminBundle/issues/1586
if (null !== $sortBy) {
$rootAliasSortBy = strstr($sortBy, '.', true);

if ($rootAliasSortBy !== $rootAlias) {
$query->getQueryBuilder()->addSelect($rootAliasSortBy);
if (false !== $rootAliasSortBy && $rootAliasSortBy !== $rootAlias) {
$this->isOneToMany($query, $rootAliasSortBy) ?
$query->setSortBy([], []) :
$query->getQueryBuilder()->addSelect($rootAliasSortBy);
}
}

Expand All @@ -50,4 +55,53 @@ public function createIterator(BaseProxyQueryInterface $query, array $fields): \

return new DoctrineORMQuerySourceIterator($query->getDoctrineQuery(), $fields);
}

/**
* @param ProxyQueryInterface<object> $query
*/
private function isOneToMany(ProxyQueryInterface $query, string $alias): bool
{
$fieldName = $this->findFieldName($query, $alias);

if (null === $fieldName) {
return false;
}

$rootEntity = current($query->getQueryBuilder()->getRootEntities());

if (false === $rootEntity) {
return false;
}

$associationMapping = $query
->getQueryBuilder()
->getEntityManager()
->getClassMetadata($rootEntity)
->getAssociationMapping($fieldName);

return ClassMetadata::ONE_TO_MANY === $associationMapping['type'];
}

/**
* @param ProxyQueryInterface<object> $query
*/
private function findFieldName(ProxyQueryInterface $query, string $alias): ?string
{
$joins = $query->getQueryBuilder()->getDQLPart('join');

foreach ($joins as $joined) {
foreach ($joined as $joinPart) {
$join = $joinPart->getJoin();
$joinAlias = $joinPart->getAlias();

if ($joinAlias === $alias) {
$joinParts = explode('.', $join);

return end($joinParts);
}
}
}

return null;
}
}
9 changes: 9 additions & 0 deletions tests/App/Admin/AuthorAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ protected function configureListFields(ListMapper $list): void
])
->add('numberOfReaders', FieldDescriptionInterface::TYPE_INTEGER, [
'template' => 'author/list_number_of_readers_field.html.twig',
])
->addIdentifier('firstBook', null, [
'sortable' => true,
'sort_field_mapping' => [
'fieldName' => 'name',
],
'sort_parent_association_mappings' => [[
'fieldName' => 'books',
]],
]);
}

Expand Down
20 changes: 18 additions & 2 deletions tests/App/Admin/ItemAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,24 @@ final class ItemAdmin extends AbstractAdmin
protected function configureListFields(ListMapper $list): void
{
$list
->add('command')
->add('product')
->add('command.id', null, [
'sortable' => true,
'sort_field_mapping' => [
'fieldName' => 'id',
],
'sort_parent_association_mappings' => [[
'fieldName' => 'command',
]],
])
->add('product.name', null, [
'sortable' => true,
'sort_field_mapping' => [
'fieldName' => 'name',
],
'sort_parent_association_mappings' => [[
'fieldName' => 'product',
]],
])
->add('offeredPrice');
}
}
2 changes: 2 additions & 0 deletions tests/App/AppKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Sonata\BlockBundle\SonataBlockBundle;
use Sonata\Doctrine\Bridge\Symfony\SonataDoctrineBundle;
use Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle;
use Sonata\Exporter\Bridge\Symfony\SonataExporterBundle;
use Sonata\Form\Bridge\Symfony\SonataFormBundle;
use Sonata\Twig\Bridge\Symfony\SonataTwigBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
Expand Down Expand Up @@ -49,6 +50,7 @@ public function registerBundles(): iterable
new SecurityBundle(),
new SonataAdminBundle(),
new SonataBlockBundle(),
new SonataExporterBundle(),
new SonataDoctrineBundle(),
new SonataDoctrineORMAdminBundle(),
new SonataFormBundle(),
Expand Down
7 changes: 7 additions & 0 deletions tests/App/Entity/Author.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,11 @@ public function getNumberOfReaders(): int
)->toArray()
);
}

public function getFirstBook(): ?Book
{
$firstBook = $this->books->first();

return false !== $firstBook ? $firstBook : null;
}
}
7 changes: 7 additions & 0 deletions tests/Datagrid/ProxyQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,15 @@ public function testExecuteWithOrderBy(): void
$query2 = new ProxyQuery(
$this->em->createQueryBuilder()->select('o')->from(DoubleNameEntity::class, 'o')->addOrderBy('o.name')
);

static::assertSame([$entity2, $entity3, $entity1], iterator_to_array($query2->execute()));

$query2->setSortBy([], ['fieldName' => 'name2'])->setSortOrder('ASC');

static::assertSame([$entity2, $entity1, $entity3], iterator_to_array($query2->execute()));

$query2->setSortBy([], []);

static::assertSame([$entity2, $entity3, $entity1], iterator_to_array($query2->execute()));
}
}
3 changes: 2 additions & 1 deletion tests/Exporter/DataSourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function testCreateIterator(): void
static::assertInstanceOf(DoctrineORMQuerySourceIterator::class, $iterator);
}

public function testCreateIteratorWithSortBy(): void
public function testCreateIteratorWithNormalSortBy(): void
{
$configuration = $this->createStub(Configuration::class);
$configuration->method('getDefaultQueryHints')->willReturn([]);
Expand All @@ -73,6 +73,7 @@ public function testCreateIteratorWithSortBy(): void

$query = new Query($em);

$queryBuilder->expects(static::once())->method('getDQLPart')->willReturn([]);
$queryBuilder->expects(static::once())->method('getRootAliases')->willReturn(['o', 'a', 'e']);
$queryBuilder->expects(static::once())->method('distinct');
$queryBuilder->expects(static::once())->method('select')->with('o');
Expand Down
7 changes: 6 additions & 1 deletion tests/Functional/EmbeddedMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ public function testCreateEntityWithEmbedded(): void
$attributeAddressStreet = $crawler->filter('.author_address')->attr('name');
static::assertNotNull($attributeAddressStreet);

$form = $crawler->selectButton('Create and return to list')->form();
$form = $crawler->selectButton('Create')->form();
$form[$attributeId] = 'new_id';
$form[$attributeName] = 'A wonderful author';
$form[$attributeAddressStreet] = 'A wonderful street to live';

$this->client->submit($form);

self::assertSelectorTextContains('.alert-success', '"A wonderful author" has been successfully created.');

$this->client->clickLink('Delete');
$this->client->submitForm('Yes, delete');

self::assertSelectorTextContains('.alert-success', 'tem "A wonderful author" has been deleted successfully.');
}
}
120 changes: 120 additions & 0 deletions tests/Functional/ExportActionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sonata Project package.
*
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Sonata\DoctrineORMAdminBundle\Tests\Functional;

use Symfony\Component\HttpFoundation\Request;

final class ExportActionsTest extends BaseFunctionalTestCase
{
/**
* @dataProvider provideExportActionCases
*
* @param array<mixed> $parameters
* @param array<mixed> $expected
*/
public function testExportAction(string $url, array $parameters, array $expected): void
{
// TODO: Remove the $content variable when drop support for Symfony < 6.2
ob_start();
$this->client->request(Request::METHOD_GET, $url, $parameters);
$content = ob_get_contents();
ob_end_clean();

self::assertResponseIsSuccessful();
static::assertSame(
$expected,
json_decode(
'' !== $content && false !== $content ? $content : $this->client->getInternalResponse()->getContent(),
true,
512,
\JSON_THROW_ON_ERROR
)
);
}

/**
* @return iterable<array<string>>
*
* @phpstan-return iterable<array{0: string, 1: array<mixed>, 2: array<mixed>}>
*/
public static function provideExportActionCases(): iterable
{
yield 'Normal export' => ['/admin/tests/app/book/export', [
'format' => 'json',
], [
['Id' => 'book_1', 'Name' => 'Book 1'],
['Id' => 'book_2', 'Name' => 'Book 2'],
['Id' => 'book_id', 'Name' => 'Don Quixote'],
]];

yield 'Normal export with normal sort' => ['/admin/tests/app/book/export', [
'format' => 'json',
'filter' => [
'_sort_by' => 'name',
'_sort_order' => 'DESC',
],
], [
['Id' => 'book_id', 'Name' => 'Don Quixote'],
['Id' => 'book_2', 'Name' => 'Book 2'],
['Id' => 'book_1', 'Name' => 'Book 1'],
]];

yield 'Joined export' => ['/admin/tests/app/author/export', [
'format' => 'json',
], [
['Id' => 'anonymous', 'Name' => 'Anonymous', 'Address Street' => ''],
['Id' => 'author_with_two_books', 'Name' => 'Author with 2 books', 'Address Street' => ''],
['Id' => 'autocompletion_author', 'Name' => 'autocompletion author', 'Address Street' => ''],
['Id' => 'miguel_de_cervantes', 'Name' => 'Miguel de Cervantes', 'Address Street' => 'Somewhere in La Mancha, in a place whose name I do not care to remember'],
]];

yield 'Joined export with normal sort' => ['/admin/tests/app/author/export', [
'format' => 'json',
'filter' => [
'_sort_by' => 'id',
'_sort_order' => 'DESC',
],
], [
['Id' => 'miguel_de_cervantes', 'Name' => 'Miguel de Cervantes', 'Address Street' => 'Somewhere in La Mancha, in a place whose name I do not care to remember'],
['Id' => 'autocompletion_author', 'Name' => 'autocompletion author', 'Address Street' => ''],
['Id' => 'author_with_two_books', 'Name' => 'Author with 2 books', 'Address Street' => ''],
['Id' => 'anonymous', 'Name' => 'Anonymous', 'Address Street' => ''],
]];

yield 'Joined export with oneToMany sort' => ['/admin/tests/app/author/export', [
'format' => 'json',
'filter' => [
'_sort_by' => 'firstBook',
'_sort_order' => 'DESC',
],
], [
['Id' => 'miguel_de_cervantes', 'Name' => 'Miguel de Cervantes', 'Address Street' => 'Somewhere in La Mancha, in a place whose name I do not care to remember'],
['Id' => 'autocompletion_author', 'Name' => 'autocompletion author', 'Address Street' => ''],
['Id' => 'author_with_two_books', 'Name' => 'Author with 2 books', 'Address Street' => ''],
['Id' => 'anonymous', 'Name' => 'Anonymous', 'Address Street' => ''],
]];

yield 'Joined export with ManyToOne sort' => ['/admin/tests/app/item/export', [
'format' => 'json',
'filter' => [
'_sort_by' => 'product.name',
'_sort_order' => 'DESC',
],
], [
['Offered Price' => '3'],
['Offered Price' => '2'],
['Offered Price' => '2'],
]];
}
}
7 changes: 6 additions & 1 deletion tests/Functional/ManyToOneMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function testCreateEntityWithReferences(): void
$attributeName = $crawler->filter('.book_name')->attr('name');
static::assertNotNull($attributeName);

$form = $crawler->selectButton('Create and return to list')->form();
$form = $crawler->selectButton('Create')->form();
$form[$attributeId] = 'book_new_id';
$form[$attributeName] = 'A wonderful book';

Expand All @@ -46,6 +46,11 @@ public function testCreateEntityWithReferences(): void
$this->client->submit($form);

self::assertSelectorTextContains('.alert-success', '"A wonderful book" has been successfully created.');

$this->client->clickLink('Delete');
$this->client->submitForm('Yes, delete');

self::assertSelectorTextContains('.alert-success', 'Item "A wonderful book" has been deleted successfully.');
}

private function createAuthorForm(Crawler $crawler): Form
Expand Down

0 comments on commit 5f11ecb

Please sign in to comment.