Feature: [admin] list of users on csv format

This commit is contained in:
Julien Fastré 2023-05-20 00:03:25 +02:00
parent 1f4c51f3bc
commit 748e566c7e
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
6 changed files with 188 additions and 16 deletions

View File

@ -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\MainBundle\Controller;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use League\Csv\Writer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class UserExportController
{
public function __construct(
private UserRepositoryInterface $userRepository,
private Security $security,
private TranslatorInterface $translator,
) {
}
/**
* @param Request $request
* @param string $_format
* @return Response
* @throws \League\Csv\CannotInsertRecord
* @throws \League\Csv\Exception
* @throws \League\Csv\UnavailableStream
*
* @Route("/{_locale}/admin/main/users/export/list.{_format}", requirements={"_format": "csv"}, name="chill_main_users_export_list")
*/
public function userList(Request $request, string $_format = 'csv'): Response
{
if (!$this->security->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedHttpException('Only ROLE_ADMIN can export this list');
}
$users = $this->userRepository->findAllAsArray($request->getLocale());
$csv = Writer::createFromPath('php://temp', 'r+');
$csv->insertOne(
array_map(
fn (string $e) => $this->translator->trans('admin.users.export.' . $e),
[
'id',
'username',
'email',
'enabled',
'civility_id',
'civility_abbreviation',
'civility_name',
'label',
'mainCenter_id' ,
'mainCenter_name',
'mainScope_id',
'mainScope_name',
'userJob_id',
'userJob_name',
'currentLocation_id',
'currentLocation_name',
'mainLocation_id',
'mainLocation_name',
'absenceStart'
]
)
);
$csv->addFormatter(fn (array $row) => null !== ($row['absenceStart'] ?? null) ? array_merge($row, ['absenceStart' => $row['absenceStart']->format('Y-m-d')]) : $row);
$csv->insertAll($users);
return new StreamedResponse(
function () use ($csv) {
foreach ($csv->chunk(1024) as $chunk) {
echo $chunk;
flush();
}
},
Response::HTTP_OK,
[
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; users.csv',
]
);
}
}

View File

@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
@ -76,6 +77,52 @@ final class UserRepository implements UserRepositoryInterface
return $this->repository->findAll();
}
/**
* @param string $lang
*/
public function findAllAsArray(string $lang): iterable
{
$dql = sprintf(<<<'DQL'
SELECT
u.id AS id,
u.username AS username,
u.email,
u.enabled,
IDENTITY(u.civility) AS civility_id,
JSON_EXTRACT(civility.abbreviation, :lang) AS civility_abbreviation,
JSON_EXTRACT(civility.name, :lang) AS civility_name,
u.label,
mainCenter.id AS mainCenter_id,
mainCenter.name AS mainCenter_name,
IDENTITY(u.mainScope) AS mainScope_id,
JSON_EXTRACT(mainScope.name, :lang) AS mainScope_name,
IDENTITY(u.userJob) AS userJob_id,
JSON_EXTRACT(userJob.label, :lang) AS userJob_name,
currentLocation.id AS currentLocation_id,
currentLocation.name AS currentLocation_name,
mainLocation.id AS mainLocation_id,
mainLocation.name AS mainLocation_name,
u.absenceStart
FROM Chill\MainBundle\Entity\User u
LEFT JOIN u.civility civility
LEFT JOIN u.currentLocation currentLocation
LEFT JOIN u.mainLocation mainLocation
LEFT JOIN u.mainCenter mainCenter
LEFT JOIN u.mainScope mainScope
LEFT JOIN u.userJob userJob
ORDER BY u.label
DQL);
$query = $this->entityManager->createQuery($dql)
->setHydrationMode(AbstractQuery::HYDRATE_ARRAY)
->setParameter('lang', $lang)
;
foreach ($query->toIterable() as $u) {
yield $u;
}
}
/**
* @param mixed|null $limit
* @param mixed|null $offset

View File

@ -14,6 +14,9 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\User;
use Doctrine\Persistence\ObjectRepository;
/**
* @template ObjectRepository<User>
*/
interface UserRepositoryInterface extends ObjectRepository
{
public function countBy(array $criteria): int;
@ -24,20 +27,11 @@ interface UserRepositoryInterface extends ObjectRepository
public function countByUsernameOrEmail(string $pattern): int;
public function find($id, $lockMode = null, $lockVersion = null): ?User;
/**
* @return User[]
* @param string $lang The lang to display all the translatable string (no fallback if not present)
* @return iterable<array{id: int, username: string, email: string, enable: bool, civility_id: int, civility_abbreviation: string, civility_name: string, label: string, mainCenter_id: int, mainCenter_name: string, mainScope_id: int, mainScope_name: string, userJob_id: int, userJob_name: string, currentLocation_id: int, currentLocation_name: string, mainLocation_id: int, mainLocation_name: string, absenceStart: \DateTimeImmutable}>
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return User[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findAllAsArray(string $lang): iterable;
/**
* @return array|User[]
@ -53,8 +47,6 @@ interface UserRepositoryInterface extends ObjectRepository
public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?User;
public function findOneByUsernameOrEmail(string $pattern): ?User;
/**
@ -68,6 +60,4 @@ interface UserRepositoryInterface extends ObjectRepository
* @param mixed $flag
*/
public function findUsersHavingFlags($flag, array $amongstUsers = []): array;
public function getClassName(): string;
}

View File

@ -98,6 +98,17 @@
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
<li>
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-download"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ path('chill_main_users_export_list') }}">{{ 'admin.users.export_list_csv'|trans }}</a></li>
</ul>
</div>
</li>
<li>
<a href="{{ path('chill_crud_admin_user_new') }}" class="btn btn-create">{{ 'Create'|trans }}</a>
</li>

View File

@ -37,3 +37,6 @@ services:
Chill\MainBundle\Controller\RegroupmentController:
autowire: true
autoconfigure: true
Chill\MainBundle\Controller\UserExportController:
tags: ['controller.service_arguments']

View File

@ -610,3 +610,27 @@ absence:
You are listed as absent, as of: Votre absence est indiquée à partir du
No absence listed: Aucune absence indiquée.
Is absent: Absent?
admin:
users:
export_list_csv: Liste des utilisateurs (format CSV)
export:
id: Identifiant
username: Nom d'utilisateur
email: Courriel
enabled: Activé
civility_id: Identifiant civilité
civility_abbreviation: Abbréviation civilité
civility_name: Civilité
label: Label
mainCenter_id: Identifiant centre principal
mainCenter_name: Centre principal
mainScope_id: Identifiant service principal
mainScope_name: Service principal
userJob_id: Identifiant métier
userJob_name: Métier
currentLocation_id: Identifiant localisation actuelle
currentLocation_name: Localisation actuelle
mainLocation_id: Identifiant localisation principale
mainLocation_name: Localisation principale
absenceStart: Absent à partir du