Event: add more fields: documents, organizationCost, note, and location

This commit is contained in:
Julien Fastré 2023-11-28 17:39:38 +01:00
parent 1fbbf2b2ad
commit 6e2cce9531
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
10 changed files with 282 additions and 39 deletions

View File

@ -17,6 +17,7 @@ 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\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
@ -37,6 +38,8 @@ 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;
/**
@ -52,7 +55,8 @@ final class EventController extends AbstractController
private readonly AuthorizationHelperInterface $authorizationHelper,
private readonly FormFactoryInterface $formFactoryInterface,
private readonly TranslatorInterface $translator,
private readonly PaginatorFactory $paginator
private readonly PaginatorFactory $paginator,
private readonly Security $security,
) {}
/**
@ -202,6 +206,12 @@ final 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);
@ -209,6 +219,7 @@ final class EventController extends AbstractController
$entity = new Event();
$entity->setCenter($center);
$entity->setLocation($user->getCurrentLocation());
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
@ -336,7 +347,7 @@ final 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', [

View File

@ -11,13 +11,16 @@ 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;
@ -42,6 +45,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")A
*
* @Assert\NotNull()
*/
private ?Center $center = null;
@ -72,6 +76,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/**
* @ORM\Column(type="string", length=150)
*
* @Assert\NotBlank()
*/
private ?string $name = null;
@ -87,16 +92,45 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
/**
* @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();
}
/**
@ -111,6 +145,22 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
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
*/
@ -270,4 +320,44 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter
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;
}
}

View File

@ -20,7 +20,6 @@ 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;
@ -33,10 +32,12 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* repositoryClass="Chill\EventBundle\Repository\ParticipationRepository")
*
* @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, TrackUpdateInterface, TrackCreationInterface
@ -62,6 +63,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
*
* @Assert\NotNull()
*/
private ?Person $person = null;
@ -73,14 +75,12 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/**
* @ORM\ManyToOne(targetEntity="Chill\EventBundle\Entity\Status")
*
* @Assert\NotNull()
*/
private ?Status $status = null;
/**
* @return Center
*/
public function getCenter(): null|Center
public function getCenter()
{
if (null === $this->getEvent()) {
throw new \RuntimeException('The event is not linked with this instance. You should initialize the event with a valid center before.');
@ -99,8 +99,6 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/**
* Get id.
*
* @return int
*/
public function getId(): int
{
@ -110,7 +108,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
/**
* Get lastUpdate.
*
* @return \DateTime
* @return \DateTimeInterface|null
*/
public function getLastUpdate()
{
@ -289,6 +287,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa
* Set lastUpdate.
*
* @return Participation
*
* @deprecated
*/
protected function update()

View File

@ -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',
]);
}

View File

@ -14,10 +14,15 @@
{{ form_row(edit_form.type, { 'label': 'Event type' }) }}
{{ form_row(edit_form.moderator) }}
{{ form_row(edit_form.location) }}
{{ form_row(edit_form.organizationCost) }}
{{ form_row(edit_form.comment) }}
{{ form_row(edit_form.documents) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_path_add_return_path('chill_event_event_list') }}" class="btn btn-cancel">
<a href="{{ chill_return_path_or('chill_event_event_list') }}" class="btn btn-cancel">
{{ 'List of events'|trans|chill_return_path_label }}
</a>
</li>

View File

@ -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 }}

View File

@ -7,11 +7,13 @@
{% 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 -%}
@ -20,6 +22,10 @@
<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>
@ -32,42 +38,58 @@
<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>
{{ 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 %}
</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>
<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="{{ 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">
<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>
{% endif %}
<li>
<a href="{{ path('chill_event__event_delete', {'event_id' : event.id } ) }}"
class="btn btn-delete">{{ 'Delete event'|trans }}</a>
@ -93,7 +115,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,
@ -104,7 +134,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>

View File

@ -2,6 +2,13 @@
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;

View File

@ -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');
}
}

View File

@ -110,6 +110,15 @@ 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