Skip to content

Commit

Permalink
new: Add observers to a tickets
Browse files Browse the repository at this point in the history
  • Loading branch information
marien-probesys committed Aug 13, 2024
2 parents 8b35064 + 1cac918 commit 22a4dd3
Show file tree
Hide file tree
Showing 16 changed files with 420 additions and 33 deletions.
54 changes: 54 additions & 0 deletions migrations/Version20240813123605AddObserversToTicket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

// This file is part of Bileto.
// Copyright 2022-2024 Probesys
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20240813123605AddObserversToTicket extends AbstractMigration
{
public function getDescription(): string
{
return 'Add the observers relation to the ticket table';
}

public function up(Schema $schema): void
{
// phpcs:disable Generic.Files.LineLength
$dbPlatform = $this->connection->getDatabasePlatform();
if ($dbPlatform instanceof PostgreSQLPlatform) {
$this->addSql('CREATE TABLE ticket_user (ticket_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(ticket_id, user_id))');
$this->addSql('CREATE INDEX IDX_BF48C371700047D2 ON ticket_user (ticket_id)');
$this->addSql('CREATE INDEX IDX_BF48C371A76ED395 ON ticket_user (user_id)');
$this->addSql('ALTER TABLE ticket_user ADD CONSTRAINT FK_BF48C371700047D2 FOREIGN KEY (ticket_id) REFERENCES ticket (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE ticket_user ADD CONSTRAINT FK_BF48C371A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
} elseif ($dbPlatform instanceof MariaDBPlatform) {
$this->addSql('CREATE TABLE ticket_user (ticket_id INT NOT NULL, user_id INT NOT NULL, INDEX IDX_BF48C371700047D2 (ticket_id), INDEX IDX_BF48C371A76ED395 (user_id), PRIMARY KEY(ticket_id, user_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`');
$this->addSql('ALTER TABLE ticket_user ADD CONSTRAINT FK_BF48C371700047D2 FOREIGN KEY (ticket_id) REFERENCES ticket (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE ticket_user ADD CONSTRAINT FK_BF48C371A76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id) ON DELETE CASCADE');
}
// phpcs:enable
}

public function down(Schema $schema): void
{
$dbPlatform = $this->connection->getDatabasePlatform();
if ($dbPlatform instanceof PostgreSQLPlatform) {
$this->addSql('ALTER TABLE ticket_user DROP CONSTRAINT FK_BF48C371700047D2');
$this->addSql('ALTER TABLE ticket_user DROP CONSTRAINT FK_BF48C371A76ED395');
$this->addSql('DROP TABLE ticket_user');
} elseif ($dbPlatform instanceof MariaDBPlatform) {
$this->addSql('ALTER TABLE ticket_user DROP FOREIGN KEY FK_BF48C371700047D2');
$this->addSql('ALTER TABLE ticket_user DROP FOREIGN KEY FK_BF48C371A76ED395');
$this->addSql('DROP TABLE ticket_user');
}
}
}
52 changes: 30 additions & 22 deletions src/Controller/Tickets/ActorsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
namespace App\Controller\Tickets;

use App\Controller\BaseController;
use App\Entity\Ticket;
use App\Entity;
use App\Form;
use App\Repository\TeamRepository;
use App\Repository\TicketRepository;
use App\Repository\UserRepository;
use App\Repository;
use App\Service\ActorsLister;
use App\Service\Sorter\TeamSorter;
use App\TicketActivity\TicketEvent;
Expand All @@ -27,16 +25,12 @@
class ActorsController extends BaseController
{
#[Route('/tickets/{uid:ticket}/actors/edit', name: 'edit ticket actors', methods: ['GET', 'HEAD'])]
public function edit(
Ticket $ticket,
ActorsLister $actorsLister,
TeamSorter $teamSorter,
TeamRepository $teamRepository,
): Response {
public function edit(Entity\Ticket $ticket): Response
{
$organization = $ticket->getOrganization();
$this->denyAccessUnlessGranted('orga:update:tickets:actors', $organization);

/** @var \App\Entity\User */
/** @var Entity\User */
$user = $this->getUser();

if (!$ticket->hasActor($user)) {
Expand All @@ -53,28 +47,25 @@ public function edit(

#[Route('/tickets/{uid:ticket}/actors/edit', name: 'update ticket actors', methods: ['POST'])]
public function update(
Ticket $ticket,
Entity\Ticket $ticket,
Request $request,
TeamRepository $teamRepository,
TicketRepository $ticketRepository,
UserRepository $userRepository,
ActorsLister $actorsLister,
TeamSorter $teamSorter,
ValidatorInterface $validator,
TranslatorInterface $translator,
Repository\TicketRepository $ticketRepository,
Repository\EntityEventRepository $entityEventRepository,
EventDispatcherInterface $eventDispatcher,
): Response {
$organization = $ticket->getOrganization();
$this->denyAccessUnlessGranted('orga:update:tickets:actors', $organization);

/** @var \App\Entity\User */
/** @var Entity\User */
$user = $this->getUser();

if (!$ticket->hasActor($user)) {
$this->denyAccessUnlessGranted('orga:see:tickets:all', $organization);
}

$previousAssignee = $ticket->getAssignee();
$initialObservers = $ticket->getObservers()->toArray();
$initialObserversIds = array_map(fn (Entity\User $observer): int => $observer->getId(), $initialObservers);
$initialAssignee = $ticket->getAssignee();

$form = $this->createNamedForm('ticket_actors', Form\Ticket\ActorsForm::class, $ticket);
$form->handleRequest($request);
Expand All @@ -89,13 +80,30 @@ public function update(
$ticket = $form->getData();
$ticketRepository->save($ticket, true);

$newObservers = $ticket->getObservers()->toArray();
$newObserversIds = array_map(fn (Entity\User $observer): int => $observer->getId(), $newObservers);
$newAssignee = $ticket->getAssignee();

if ($previousAssignee != $newAssignee) {
if ($initialAssignee != $newAssignee) {
$ticketEvent = new TicketEvent($ticket);
$eventDispatcher->dispatch($ticketEvent, TicketEvent::ASSIGNED);
}

// Log changes to the observers field manually, as we cannot log
// these automatically with the EntityActivitySubscriber (i.e. ManyToMany
// relationships cannot be handled easily).
if ($initialObserversIds != $newObserversIds) {
$changes = [
$initialObserversIds,
$newObserversIds,
];

$entityEvent = Entity\EntityEvent::initUpdate($ticket, [
'observers' => $changes,
]);
$entityEventRepository->save($entityEvent, true);
}

return $this->redirectToRoute('ticket', [
'uid' => $ticket->getUid(),
]);
Expand Down
30 changes: 30 additions & 0 deletions src/Entity/Ticket.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class Ticket implements MonitorableEntityInterface, UidEntityInterface
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Team $team = null;

/** @var Collection<int, User> */
#[ORM\ManyToMany(targetEntity: User::class)]
private Collection $observers;

#[ORM\ManyToOne(inversedBy: 'tickets')]
#[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")]
private ?Organization $organization = null;
Expand Down Expand Up @@ -156,6 +160,7 @@ public function __construct()
$this->contracts = new ArrayCollection();
$this->timeSpents = new ArrayCollection();
$this->labels = new ArrayCollection();
$this->observers = new ArrayCollection();
}

public function getType(): ?string
Expand Down Expand Up @@ -356,6 +361,7 @@ public function hasActor(User $user): bool
$this->createdBy->getId() === $userId ||
($this->requester && $this->requester->getId() === $userId) ||
($this->assignee && $this->assignee->getId() === $userId) ||
$this->observers->contains($user) ||
($this->team && $this->team->hasAgent($user))
);
}
Expand Down Expand Up @@ -584,4 +590,28 @@ public function setLabels(array $labels): static

return $this;
}

/**
* @return Collection<int, User>
*/
public function getObservers(): Collection
{
return $this->observers;
}

public function addObserver(User $observer): static
{
if (!$this->observers->contains($observer)) {
$this->observers->add($observer);
}

return $this;
}

public function removeObserver(User $observer): static
{
$this->observers->removeElement($observer);

return $this;
}
}
5 changes: 5 additions & 0 deletions src/Form/Ticket/ActorsForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'organization' => $organization,
'roleType' => 'agent',
]);

$form->add('observers', Type\ActorType::class, [
'organization' => $organization,
'multiple' => true,
]);
});

$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
Expand Down
7 changes: 7 additions & 0 deletions src/MessageHandler/SendMessageEmailHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function __invoke(SendMessageEmail $data): void

$author = $message->getCreatedBy();
$requester = $ticket->getRequester();
$observers = $ticket->getObservers();
$assignee = $ticket->getAssignee();

$recipients = [];
Expand All @@ -58,6 +59,12 @@ public function __invoke(SendMessageEmail $data): void
$recipients[] = $requester->getEmail();
}

foreach ($observers as $observer) {
if ($observer !== $author) {
$recipients[] = $observer->getEmail();
}
}

if ($assignee && $assignee !== $author) {
$recipients[] = $assignee->getEmail();
}
Expand Down
15 changes: 14 additions & 1 deletion src/SearchEngine/QueryBuilder/TicketQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ private function buildQualifierExpr(SearchEngine\Query\Condition $condition): st
$value = $this->processActorQualifier($value);
$field = "COALESCE(IDENTITY(t.{$qualifier}), 0)";
return $this->buildExpr($field, $value, $condition->not());
} elseif ($qualifier === 'observer') {
$value = $this->processActorQualifier($value);

$subBuilder = $this->buildManyToManyQueryBuilder('App\Entity\Ticket', 'observers', $value);

if ($condition->not()) {
return "t.id NOT IN ({$subBuilder->getDQL()})";
} else {
return "t.id IN ({$subBuilder->getDQL()})";
}
} elseif ($qualifier === 'involves') {
$value = $this->processActorQualifier($value);

Expand All @@ -162,7 +172,10 @@ private function buildQualifierExpr(SearchEngine\Query\Condition $condition): st
$subTeamBuilder = $this->buildManyToManyQueryBuilder('App\Entity\Team', 'agents', $value);
$teamWhere = "COALESCE(IDENTITY(t.team), 0) IN ({$subTeamBuilder->getDQL()})";

$where = "{$assigneeWhere} OR {$requesterWhere} OR {$teamWhere}";
$subObserversBuilder = $this->buildManyToManyQueryBuilder('App\Entity\Ticket', 'observers', $value);
$observersWhere = "t.id IN ({$subObserversBuilder->getDQL()})";

$where = "{$assigneeWhere} OR {$requesterWhere} OR {$teamWhere} OR {$observersWhere}";

if ($condition->not()) {
return "NOT ({$where})";
Expand Down
57 changes: 57 additions & 0 deletions src/Twig/TicketEventChangesFormatterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public function formatTicketChanges(Entity\EntityEvent $event, string $field): s
return $this->formatOngoingContractChanges($user, $fieldChanges);
} elseif ($field === 'labels') {
return $this->formatLabelsChanges($user, $fieldChanges);
} elseif ($field === 'observers') {
return $this->formatObserversChanges($user, $fieldChanges);
} else {
return $this->formatChanges($user, $field, $fieldChanges);
}
Expand Down Expand Up @@ -402,6 +404,61 @@ private function formatLabelsChanges(Entity\User $user, array $changes): string
}
}

/**
* @param array<int[]> $changes
*/
private function formatObserversChanges(Entity\User $user, array $changes): string
{
$username = $this->escape($user->getDisplayName());

$removedObserversIds = array_diff($changes[0], $changes[1]);
$addedObserversIds = array_diff($changes[1], $changes[0]);

$removedObservers = $this->userRepository->findBy([
'id' => $removedObserversIds,
]);
$addedObservers = $this->userRepository->findBy([
'id' => $addedObserversIds,
]);

$removed = array_map(function ($user): string {
return $this->escape($user->getDisplayName());
}, $removedObservers);
$removed = implode(', ', $removed);

$added = array_map(function ($user): string {
return $this->escape($user->getDisplayName());
}, $addedObservers);
$added = implode(', ', $added);

if (empty($removedObserversIds)) {
return $this->translator->trans(
'tickets.events.observers.added',
[
'username' => $username,
'added' => $added,
],
);
} elseif (empty($addedObserversIds)) {
return $this->translator->trans(
'tickets.events.observers.removed',
[
'username' => $username,
'removed' => $removed,
],
);
} else {
return $this->translator->trans(
'tickets.events.observers.added_and_removed',
[
'username' => $username,
'added' => $added,
'removed' => $removed,
],
);
}
}

/**
* @param mixed[] $changes
*/
Expand Down
11 changes: 8 additions & 3 deletions templates/pages/advanced_search_syntax.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,24 @@
</tr>

<tr>
<td><code>involves:alix</code></td>
<td><code>observer:@me</code></td>
<td>{{ 'advanced_search_syntax.qualifiers.actors.example3' | trans }}</td>
</tr>

<tr>
<td><code>no:assignee</code></td>
<td><code>involves:alix</code></td>
<td>{{ 'advanced_search_syntax.qualifiers.actors.example4' | trans }}</td>
</tr>

<tr>
<td><code>has:assignee</code></td>
<td><code>no:assignee</code></td>
<td>{{ 'advanced_search_syntax.qualifiers.actors.example5' | trans }}</td>
</tr>

<tr>
<td><code>has:assignee</code></td>
<td>{{ 'advanced_search_syntax.qualifiers.actors.example6' | trans }}</td>
</tr>
</tbody>
</table>
</div>
Expand Down
Loading

0 comments on commit 22a4dd3

Please sign in to comment.