Create event theme admin entity

This commit is contained in:
Julie Lenaerts 2025-04-28 16:32:48 +02:00
parent 3e7f03d331
commit c1530d701f
15 changed files with 608 additions and 7 deletions

View File

@ -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\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
class EventThemeController extends CRUDController
{
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
{
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'create']);
}
if ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, ['step' => 'edit']);
}
throw new \LogicException('action is not supported: '.$action);
}
/**
* @param QueryBuilder|mixed $query
*/
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator): QueryBuilder
{
/* @var QueryBuilder $query */
return $query->orderBy('e.ordering', 'ASC')
->addOrderBy('e.id', 'ASC');
}
}

View File

@ -11,6 +11,9 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection; namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Controller\EventThemeController;
use Chill\EventBundle\Entity\EventTheme;
use Chill\EventBundle\Form\EventThemeType;
use Chill\EventBundle\Security\EventVoter; use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter; use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
@ -26,7 +29,10 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
*/ */
class ChillEventExtension extends Extension implements PrependExtensionInterface class ChillEventExtension extends Extension implements PrependExtensionInterface
{ {
public function load(array $configs, ContainerBuilder $container) /**
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container): void
{ {
$configuration = new Configuration(); $configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
@ -45,16 +51,17 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** (non-PHPdoc). /** (non-PHPdoc).
* @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend() * @see \Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface::prepend()
*/ */
public function prepend(ContainerBuilder $container) public function prepend(ContainerBuilder $container): void
{ {
$this->prependAuthorization($container); $this->prependAuthorization($container);
$this->prependCruds($container);
$this->prependRoute($container); $this->prependRoute($container);
} }
/** /**
* add authorization hierarchy. * add authorization hierarchy.
*/ */
protected function prependAuthorization(ContainerBuilder $container) protected function prependAuthorization(ContainerBuilder $container): void
{ {
$container->prependExtensionConfig('security', [ $container->prependExtensionConfig('security', [
'role_hierarchy' => [ 'role_hierarchy' => [
@ -70,7 +77,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
/** /**
* add route to route loader for chill. * add route to route loader for chill.
*/ */
protected function prependRoute(ContainerBuilder $container) protected function prependRoute(ContainerBuilder $container): void
{ {
// add routes for custom bundle // add routes for custom bundle
$container->prependExtensionConfig('chill_main', [ $container->prependExtensionConfig('chill_main', [
@ -81,4 +88,33 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
], ],
]); ]);
} }
protected function prependCruds(ContainerBuilder $container): void
{
$container->prependExtensionConfig('chill_main', [
'cruds' => [
[
'class' => EventTheme::class,
'name' => 'event_theme',
'base_path' => '/admin/event/theme',
'form_class' => EventThemeType::class,
'controller' => EventThemeController::class,
'actions' => [
'index' => [
'template' => '@ChillEvent/Admin/EventTheme/index.html.twig',
'role' => 'ROLE_ADMIN',
],
'new' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/new.html.twig',
],
'edit' => [
'role' => 'ROLE_ADMIN',
'template' => '@ChillEvent/Admin/EventTheme/edit.html.twig',
],
],
],
],
]);
}
} }

View File

@ -0,0 +1,183 @@
<?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\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
/**
* Class EventTheme.
*/
#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_event_event_theme')]
class EventTheme
{
#[ORM\Column(type: Types::BOOLEAN, nullable: false)]
private bool $isActive = true;
#[ORM\Id]
#[ORM\Column(name: 'id', type: Types::INTEGER)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\Column(type: Types::JSON)]
private array $name;
/**
* @var Collection<int, EventTheme>
*/
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: EventTheme::class)]
private Collection $children;
#[ORM\ManyToOne(targetEntity: EventTheme::class, inversedBy: 'children')]
private ?EventTheme $parent = null;
#[ORM\Column(name: 'ordering', type: Types::FLOAT, options: ['default' => '0.0'])]
private float $ordering = 0.0;
/**
* Constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
/**
* Get active.
*/
public function getIsActive(): bool
{
return $this->isActive;
}
/**
* Get id.
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Get label.
*/
public function getName(): array
{
return $this->name;
}
/**
* Set active.
*
* @param bool $active
* @return EventTheme
*/
public function setIsActive(bool $active): static
{
$this->isActive = $active;
return $this;
}
/**
* Set label.
*
* @param array $label
* @return EventTheme
*/
public function setName(array $label): static
{
$this->name = $label;
return $this;
}
public function addChild(self $child): self
{
if (!$this->children->contains($child)) {
$this->children[] = $child;
}
return $this;
}
public function removeChild(self $child): self
{
if ($this->children->removeElement($child)) {
// set the owning side to null (unless already changed)
if ($child->getParent() === $this) {
$child->setParent(null);
}
}
return $this;
}
public function getChildren(): Collection
{
return $this->children;
}
public function getDescendants(): Collection
{
$descendants = new ArrayCollection();
foreach ($this->getChildren() as $child) {
if (!$descendants->contains($child)) {
$descendants->add($child);
foreach ($child->getDescendants() as $descendantsOfChild) {
if (!$descendants->contains($descendantsOfChild)) {
$descendants->add($descendantsOfChild);
}
}
}
}
return $descendants;
}
public function hasParent(): bool
{
return null !== $this->parent;
}
public function getOrdering(): float
{
return $this->ordering;
}
public function setOrdering(float $ordering): EventTheme
{
$this->ordering = $ordering;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
$parent?->addChild($this);
return $this;
}
}

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\EventBundle\Form;
use Chill\EventBundle\Entity\EventTheme;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\AbstractType;
class EventThemeType extends AbstractType
{
public function __construct(protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TranslatableStringFormType::class, [
'label' => 'Nom',
]);
if ('create' === $options['step']) {
$builder
->add('parent', EntityType::class, [
'class' => EventTheme::class,
'required' => false,
'choice_label' => fn (EventTheme $theme): ?string => $this->translatableStringHelper->localize($theme->getName()),
'mapped' => 'create' === $options['step'],
]);
}
$builder
->add('ordering', NumberType::class, [
'required' => true,
'scale' => 6,
])
->add('isActive', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
],
'expanded' => true,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => EventTheme::class,
]);
$resolver->setRequired('step')
->setAllowedValues('step', ['create', 'edit']);
}
}

View File

@ -20,14 +20,14 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface
/** /**
* @var AuthorizationCheckerInterface * @var AuthorizationCheckerInterface
*/ */
protected $authorizationChecker; protected AuthorizationCheckerInterface $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker) public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{ {
$this->authorizationChecker = $authorizationChecker; $this->authorizationChecker = $authorizationChecker;
} }
public function buildMenu($menuId, MenuItem $menu, array $parameters) public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{ {
if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
return; return;
@ -52,6 +52,10 @@ class AdminMenuBuilder implements LocalMenuBuilderInterface
$menu->addChild('Role', [ $menu->addChild('Role', [
'route' => 'chill_event_admin_role', 'route' => 'chill_event_admin_role',
])->setExtras(['order' => 6530]); ])->setExtras(['order' => 6530]);
$menu->addChild('Theme', [
'route' => 'chill_crud_event_theme_index',
])->setExtras(['order' => 6540]);
} }
public static function getMenuIds(): array public static function getMenuIds(): array

View File

@ -0,0 +1,26 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block crud_content_form_rows %}
{{ form_row(form.name) }}
<div class="mb-3 row">
<label class="col-form-label col-sm-4">
{{ 'Parent'|trans }}
</label>
<div class="col-sm-8">
{{ entity.parent|chill_entity_render_box }}
</div>
</div>
{{ form_row(form.ordering) }}
{{ form_row(form.isActive) }}
{% endblock crud_content_form_rows %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@ -0,0 +1,45 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>{{ 'Id'|trans }}</th>
<th>{{ 'Title'|trans }}</th>
<th>{{ 'Ordering'|trans }}</th>
<th>{{ 'active'|trans }}</th>
<th>&nbsp;</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr>
<td>{{ entity.id }}</td>
<td>
{{ entity|chill_entity_render_box }}
</td>
<td>{{ entity.ordering }}</td>
<td style="text-align:center;">
{%- if entity.isActive -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_event_theme_edit', { 'id': entity.id }) }}" class="btn btn-edit"></a>
</li>
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block actions_before %}
<li class='cancel'>
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans}}</a>
</li>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
{% block admin_content %}
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock admin_content %}

View File

@ -0,0 +1,13 @@
{% set reversed_parents = parents|reverse %}
<span class="chill-entity entity-event-theme">
<span class="badge bg-chill-l-gray text-dark">
{%- for p in reversed_parents %}
<span class="parent-{{ loop.revindex0 }}">
{{ p.name|localize_translatable_string }}{{ options['default.separator'] }}
</span>
{%- endfor -%}
<span class="child">
{{ eventTheme.name|localize_translatable_string }}
</span>
</span>
</span>

View File

@ -0,0 +1,109 @@
<?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\Templating\Entity;
use Chill\EventBundle\Entity\EventTheme;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
/**
* @implements ChillEntityRenderInterface<EventTheme>
*/
class EventThemeRender implements ChillEntityRenderInterface
{
public const AND_CHILDREN_MENTION = 'show_and_children_mention';
public const DEFAULT_ARGS = [
self::SEPARATOR_KEY => ' > ',
self::SHOW_AND_CHILDREN => false,
self::AND_CHILDREN_MENTION => 'event_theme.and children',
];
public const SEPARATOR_KEY = 'default.separator';
/**
* Show a mention "and children" on each EventTheme, if the event theme
* has at least one child.
*/
public const SHOW_AND_CHILDREN = 'show_and_children';
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
/**
* @throws RuntimeError
* @throws SyntaxError
* @throws LoaderError
*/
public function renderBox($eventTheme, array $options): string
{
$options = array_merge(self::DEFAULT_ARGS, $options);
// give some help to twig: an array of parents
$parents = $this->buildParents($eventTheme);
return $this
->engine
->render(
'@ChillEvent/Entity/event_theme.html.twig',
[
'eventTheme' => $eventTheme,
'parents' => $parents,
'options' => $options,
]
);
}
public function renderString($entity, array $options): string
{
/** @var EventTheme $entity */
$options = array_merge(self::DEFAULT_ARGS, $options);
$titles = [$this->translatableStringHelper->localize($entity->getName())];
// loop to parent, until root
while ($entity->hasParent()) {
$entity = $entity->getParent();
$titles[] = $this->translatableStringHelper->localize(
$entity->getTitle()
);
}
$titles = \array_reverse($titles);
$title = \implode($options[self::SEPARATOR_KEY], $titles);
if ($options[self::SHOW_AND_CHILDREN] && $entity->hasChildren()) {
$title .= ' ('.$this->translator->trans($options[self::AND_CHILDREN_MENTION]).')';
}
return $title;
}
public function supports($entity, array $options): bool
{
return $entity instanceof EventTheme;
}
private function buildParents(EventTheme $entity): array
{
$parents = [];
while ($entity->hasParent()) {
$entity = $parents[] = $entity->getParent();
}
return $parents;
}
}

View File

@ -1,6 +1,7 @@
services: services:
Chill\EventBundle\Controller\: Chill\EventBundle\Controller\:
autowire: true autowire: true
autoconfigure: true
resource: '../Controller' resource: '../Controller'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
@ -9,3 +10,10 @@ services:
autoconfigure: true autoconfigure: true
resource: '../Menu/' resource: '../Menu/'
tags: ['chill.menu_builder'] tags: ['chill.menu_builder']
Chill\EventBundle\Templating\Entity\:
autowire: true
autoconfigure: true
resource: '../Templating/Entity'
tags:
- 'chill.render_entity'

View File

@ -0,0 +1,4 @@
services:
_defaults:
autowire: true
autoconfigure: true

View File

@ -31,3 +31,7 @@ services:
Chill\EventBundle\Form\Type\PickEventType: Chill\EventBundle\Form\Type\PickEventType:
tags: tags:
- { name: form.type } - { name: form.type }
Chill\EventBundle\Form\EventThemeType:
tags:
- { name: form.type }

View File

@ -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;
/**
* Add event theme entity.
*/
final class Version20250428092611 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add an event theme entity';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_event_event_theme_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_event_event_theme (id INT NOT NULL, parent_id INT DEFAULT NULL, isActive BOOLEAN NOT NULL, name JSON NOT NULL, ordering DOUBLE PRECISION DEFAULT \'0.0\' NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_80D7C6B0727ACA70 ON chill_event_event_theme (parent_id)');
$this->addSql('ALTER TABLE chill_event_event_theme ADD CONSTRAINT FK_80D7C6B0727ACA70 FOREIGN KEY (parent_id) REFERENCES chill_event_event_theme (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_event_event_theme DROP CONSTRAINT FK_80D7C6B0727ACA70');
$this->addSql('DROP TABLE chill_event_event_theme');
}
}

View File

@ -140,3 +140,10 @@ event:
event_types: Par types d'événement event_types: Par types d'événement
event_dates: Par date d'événement event_dates: Par date d'événement
crud:
event_theme:
title_new: Créér une nouvelle thématique
title_edit: Modifier la thématique
index:
title: Liste des thématiques
add_new: Créér une nouvelle thématique