Browse Source

essaie de trouver les changesets

master
vincent 4 months ago
parent
commit
41dde136e9
11 changed files with 495 additions and 41 deletions
  1. +54
    -0
      migrations/Version20240802085751.php
  2. +57
    -0
      migrations/Version20240802090033.php
  3. +44
    -0
      migrations/Version20240802120611.php
  4. +35
    -31
      src/Controller/TaskController.php
  5. +1
    -1
      src/Entity/Project.php
  6. +61
    -1
      src/Entity/Task.php
  7. +120
    -0
      src/EventSubscriber/TaskLifecycleSubscriber.php
  8. +8
    -8
      src/Security/OpenStreetMapAuthenticator.php
  9. +84
    -0
      src/Service/OpenStreetMapClient.php
  10. +29
    -0
      src/Service/OsmoseClient.php
  11. +2
    -0
      templates/partials/_task-locking.html.twig

+ 54
- 0
migrations/Version20240802085751.php View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240802085751 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__task AS SELECT id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM task');
$this->addSql('DROP TABLE task');
$this->addSql('CREATE TABLE task (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, locked_by_id INTEGER DEFAULT NULL, done_by_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, urgent INTEGER DEFAULT NULL, important INTEGER DEFAULT NULL, status VARCHAR(255) NOT NULL, geojson CLOB NOT NULL, osm CLOB DEFAULT NULL, description CLOB DEFAULT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, locked_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, doing_start_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, doing_end_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_527EDB25166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB257A88E00 FOREIGN KEY (locked_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB2535AE3EF9 FOREIGN KEY (done_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO task (id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at) SELECT id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM __temp__task');
$this->addSql('DROP TABLE __temp__task');
$this->addSql('CREATE INDEX IDX_527EDB257A88E00 ON task (locked_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB25B03A8386 ON task (created_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB25166D1F9C ON task (project_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_527EDB25989D9B62 ON task (slug)');
$this->addSql('CREATE INDEX IDX_527EDB2535AE3EF9 ON task (done_by_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__task AS SELECT id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM task');
$this->addSql('DROP TABLE task');
$this->addSql('CREATE TABLE task (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, locked_by_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, urgent INTEGER DEFAULT NULL, important INTEGER DEFAULT NULL, status VARCHAR(255) NOT NULL, geojson CLOB NOT NULL, osm CLOB DEFAULT NULL, description CLOB DEFAULT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, locked_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_527EDB25166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB257A88E00 FOREIGN KEY (locked_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO task (id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at) SELECT id, project_id, created_by_id, locked_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM __temp__task');
$this->addSql('DROP TABLE __temp__task');
$this->addSql('CREATE UNIQUE INDEX UNIQ_527EDB25989D9B62 ON task (slug)');
$this->addSql('CREATE INDEX IDX_527EDB25166D1F9C ON task (project_id)');
$this->addSql('CREATE INDEX IDX_527EDB25B03A8386 ON task (created_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB257A88E00 ON task (locked_by_id)');
}
}

+ 57
- 0
migrations/Version20240802090033.php View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240802090033 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__task AS SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM task');
$this->addSql('DROP TABLE task');
$this->addSql('CREATE TABLE task (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, locked_by_id INTEGER DEFAULT NULL, done_by_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, urgent INTEGER DEFAULT NULL, important INTEGER DEFAULT NULL, status VARCHAR(255) NOT NULL, geojson CLOB NOT NULL, osm CLOB DEFAULT NULL, description CLOB DEFAULT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, locked_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, start_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, finish_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_527EDB25166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB257A88E00 FOREIGN KEY (locked_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB2535AE3EF9 FOREIGN KEY (done_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO task (id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at) SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM __temp__task');
$this->addSql('DROP TABLE __temp__task');
$this->addSql('CREATE INDEX IDX_527EDB2535AE3EF9 ON task (done_by_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_527EDB25989D9B62 ON task (slug)');
$this->addSql('CREATE INDEX IDX_527EDB25166D1F9C ON task (project_id)');
$this->addSql('CREATE INDEX IDX_527EDB25B03A8386 ON task (created_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB257A88E00 ON task (locked_by_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__task AS SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM task');
$this->addSql('DROP TABLE task');
$this->addSql('CREATE TABLE task (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, locked_by_id INTEGER DEFAULT NULL, done_by_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, urgent INTEGER DEFAULT NULL, important INTEGER DEFAULT NULL, status VARCHAR(255) NOT NULL, geojson CLOB NOT NULL, osm CLOB DEFAULT NULL, description CLOB DEFAULT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, locked_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, doing_start_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, doing_end_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_527EDB25166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB257A88E00 FOREIGN KEY (locked_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB2535AE3EF9 FOREIGN KEY (done_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO task (id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at) SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at FROM __temp__task');
$this->addSql('DROP TABLE __temp__task');
$this->addSql('CREATE UNIQUE INDEX UNIQ_527EDB25989D9B62 ON task (slug)');
$this->addSql('CREATE INDEX IDX_527EDB25166D1F9C ON task (project_id)');
$this->addSql('CREATE INDEX IDX_527EDB25B03A8386 ON task (created_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB257A88E00 ON task (locked_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB2535AE3EF9 ON task (done_by_id)');
}
}

+ 44
- 0
migrations/Version20240802120611.php View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240802120611 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE task ADD COLUMN changesets_result CLOB DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__task AS SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at, start_at, finish_at FROM task');
$this->addSql('DROP TABLE task');
$this->addSql('CREATE TABLE task (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, locked_by_id INTEGER DEFAULT NULL, done_by_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, urgent INTEGER DEFAULT NULL, important INTEGER DEFAULT NULL, status VARCHAR(255) NOT NULL, geojson CLOB NOT NULL, osm CLOB DEFAULT NULL, description CLOB DEFAULT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, locked_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, start_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, finish_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_527EDB25166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB257A88E00 FOREIGN KEY (locked_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_527EDB2535AE3EF9 FOREIGN KEY (done_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO task (id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at, start_at, finish_at) SELECT id, project_id, created_by_id, locked_by_id, done_by_id, name, slug, urgent, important, status, geojson, osm, description, created_at, locked_at, start_at, finish_at FROM __temp__task');
$this->addSql('DROP TABLE __temp__task');
$this->addSql('CREATE UNIQUE INDEX UNIQ_527EDB25989D9B62 ON task (slug)');
$this->addSql('CREATE INDEX IDX_527EDB25166D1F9C ON task (project_id)');
$this->addSql('CREATE INDEX IDX_527EDB25B03A8386 ON task (created_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB257A88E00 ON task (locked_by_id)');
$this->addSql('CREATE INDEX IDX_527EDB2535AE3EF9 ON task (done_by_id)');
}
}

+ 35
- 31
src/Controller/TaskController.php View File

@ -8,6 +8,7 @@ use App\Entity\Task;
use App\Form\CommentType; use App\Form\CommentType;
use App\Form\TaskType; use App\Form\TaskType;
use App\Service\GeoJsonManager; use App\Service\GeoJsonManager;
use App\Service\OpenStreetMapClient;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -16,8 +17,8 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Workflow\WorkflowInterface;
#[Route('/task')] #[Route('/task')]
class TaskController extends AbstractController class TaskController extends AbstractController
@ -257,7 +258,7 @@ class TaskController extends AbstractController
return $this->redirectToRoute('app_project_show', ['slug' => $slug]); return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
} }
private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug, $transitionName, $commentTemplate): Response
private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug, $transitionName): Response
{ {
$repository = $entityManager->getRepository(Task::class); $repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($slug); $task = $repository->findOneBySlug($slug);
@ -271,35 +272,10 @@ class TaskController extends AbstractController
} }
try { try {
// TODO on doit pouvoir faire mieux pour le verrouillage, notamment au niveau des règles de tansition du workflow…
$transitions = array_filter(
$taskLifecycleStateMachine->getDefinition()->getTransitions(),
function ($transition) use ($transitionName) {
return $transitionName === $transition->getName();
}
);
$transition = reset($transitions);
$shouldTransitionLock = $taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['lock'];
$shouldTransitionUnlock = $taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['unlock'];
$taskLifecycleStateMachine->apply($task, $transitionName); $taskLifecycleStateMachine->apply($task, $transitionName);
if ($shouldTransitionLock) {
$task->lock($this->getUser());
} elseif ($shouldTransitionUnlock) {
$task->unlock();
}
$entityManager->persist($task); $entityManager->persist($task);
$comment = new Comment();
$comment->setTask($task);
$comment->setCreatedBy($this->getUser());
$comment->setContent($this->renderView($commentTemplate, [
'user' => $this->getUser(),
'task' => $task,
]));
$entityManager->persist($comment);
$entityManager->flush(); $entityManager->flush();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->addFlash( $this->addFlash(
@ -314,25 +290,25 @@ class TaskController extends AbstractController
#[Route('/{slug}/start', name: 'app_task_start')] #[Route('/{slug}/start', name: 'app_task_start')]
public function start(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response public function start(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{ {
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_START, 'comment/start.md.twig');
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_START);
} }
#[Route('/{slug}/finish', name: 'app_task_finish')] #[Route('/{slug}/finish', name: 'app_task_finish')]
public function finish(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response public function finish(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{ {
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_FINISH, 'comment/finish.md.twig');
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_FINISH);
} }
#[Route('/{slug}/cancel', name: 'app_task_cancel')] #[Route('/{slug}/cancel', name: 'app_task_cancel')]
public function cancel(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response public function cancel(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{ {
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_CANCEL, 'comment/cancel.md.twig');
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_CANCEL);
} }
#[Route('/{slug}/reset', name: 'app_task_reset')] #[Route('/{slug}/reset', name: 'app_task_reset')]
public function reset(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response public function reset(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug): Response
{ {
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_RESET, 'comment/reset.md.twig');
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_RESET);
} }
#[Route('/download/{slug}.geojson', name: 'app_task_geojson')] #[Route('/download/{slug}.geojson', name: 'app_task_geojson')]
@ -418,5 +394,33 @@ class TaskController extends AbstractController
return $response; return $response;
} }
#[Route('/{slug}/changesets', name: 'app_task_changesets')]
public function changesets(OpenStreetMapClient $osmClient, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash('warning', 'Tâche non trouvée !');
return $this->redirect($request->headers->get('referer'));
}
$task->setChangesetsResult(
json_encode(
$osmClient->getChangesets(
$task->getDoneBy()->getUsername(),
$task->getStartAt(),
$task->getFinishAt()
)
)
);
$entityManager->persist($task);
$entityManager->flush();
return $this->redirectToRoute('app_task_show', ['slug' => $task->getSlug()]);
}
} }

+ 1
- 1
src/Entity/Project.php View File

@ -269,7 +269,7 @@ class Project
$generatedAt = new \DateTimeImmutable($data['osm3s']['timestamp_osm_base']); $generatedAt = new \DateTimeImmutable($data['osm3s']['timestamp_osm_base']);
$now = new \DateTimeImmutable('now'); $now = new \DateTimeImmutable('now');
$minimum = new \DateInterval('PT1H');
$minimum = new \DateInterval('PT5M');
$isOutdated = ($generatedAt->add($minimum) < $now); $isOutdated = ($generatedAt->add($minimum) < $now);


+ 61
- 1
src/Entity/Task.php View File

@ -75,6 +75,18 @@ class Task
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $lockedAt = null; private ?\DateTimeImmutable $lockedAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $startAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $finishAt = null;
#[ORM\ManyToOne]
private ?User $doneBy = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $changesets_result = null;
public function __construct() public function __construct()
{ {
$this->comments = new ArrayCollection(); $this->comments = new ArrayCollection();
@ -266,7 +278,7 @@ class Task
public function isLocked(): bool public function isLocked(): bool
{ {
return is_null($this->lockedBy);
return !is_null($this->lockedBy);
} }
public function lock(User $user): static public function lock(User $user): static
@ -285,4 +297,52 @@ class Task
return $this; return $this;
} }
public function getStartAt(): ?\DateTimeImmutable
{
return $this->startAt;
}
public function setStartAt(?\DateTimeImmutable $startAt): static
{
$this->startAt = $startAt;
return $this;
}
public function getFinishAt(): ?\DateTimeImmutable
{
return $this->finishAt;
}
public function setFinishAt(?\DateTimeImmutable $finishAt): static
{
$this->finishAt = $finishAt;
return $this;
}
public function getDoneBy(): ?User
{
return $this->doneBy;
}
public function setDoneBy(?User $doneBy): static
{
$this->doneBy = $doneBy;
return $this;
}
public function getChangesetsResult(): ?string
{
return $this->changesets_result;
}
public function setChangesetsResult(?string $changesets_result): static
{
$this->changesets_result = $changesets_result;
return $this;
}
} }

+ 120
- 0
src/EventSubscriber/TaskLifecycleSubscriber.php View File

@ -0,0 +1,120 @@
<?php
namespace App\EventSubscriber;
use App\Entity\Comment;
use App\Entity\Task;
use App\Service\OpenStreetMapClient;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\EnteredEvent;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Event\TransitionEvent;
use Symfony\Component\Workflow\WorkflowInterface;
use Twig\Environment;
class TaskLifecycleSubscriber implements EventSubscriberInterface
{
public function __construct(
protected EntityManagerInterface $entityManager,
protected Environment $twig,
protected OpenStreetMapClient $osmClient,
protected Security $security,
protected WorkflowInterface $taskLifecycleStateMachine,
) {
}
public function onTransition(Event $event): void
{
$transition = $event->getTransition();
$task = $event->getSubject();
// Gère le verrouillage de la tâche
$shouldLock = $this->taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['lock'];
$shouldUnlock = $this->taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['unlock'];
if ($shouldLock) {
$task->lock($this->security->getUser());
} elseif ($shouldUnlock) {
$task->unlock();
}
// Commentaire automatique
$user = $this->security->getUser();
$task = $event->getSubject();
$comment = new Comment();
$comment->setTask($task);
$comment->setCreatedBy($user);
$comment->setContent($this->twig->render(sprintf('comment/%s.md.twig', $transition->getName()), [
'user' => $user,
'task' => $task,
]));
$this->entityManager->persist($comment);
$this->entityManager->flush();
}
public function onGuard(Event $event): void
{
$transition = $event->getTransition();
$task = $event->getSubject();
$shouldLock = $this->taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['lock'];
$shouldUnlock = $this->taskLifecycleStateMachine->getMetadataStore()->getTransitionMetadata($transition)['unlock'];
$subject = $event->getSubject();
if ($shouldLock and $task->isLocked()) {
$event->setBlocked(true, 'La tâche est déjà verrouillée.');
} elseif ($shouldUnlock and !$task->isLocked()) {
$event->setBlocked(true, 'La tâche n’est pas déjà verrouillée.');
}
$event->setBlocked(false);
}
public function onStart(Event $event): void
{
$task = $event->getSubject();
$task->setStartAt(new \DateTimeImmutable('now'));
}
public function onFinish(Event $event): void
{
$task = $event->getSubject();
$task->setFinishAt(new \DateTimeImmutable('now'));
}
public function onDone(Event $event): void
{
$task = $event->getSubject();
$user = $this->security->getUser();
$task->setDoneBy($user);
$task->setChangesetsResult(
json_encode($this->osmClient()->getChangesets(
$user->getUsername(),
$task->getStartAt(),
$task->getFinishAt()
))
);
}
public static function getSubscribedEvents(): array
{
return [
TransitionEvent::getName('task_lifecycle', null) => 'onTransition',
GuardEvent::getName('task_lifecycle', null) => 'onGuard',
TransitionEvent::getName('task_lifecycle', Task::TRANSITION_START) => 'onStart',
TransitionEvent::getName('task_lifecycle', Task::TRANSITION_FINISH) => 'onFinish',
EnteredEvent::getName('task_lifecycle', Task::STATUS_DONE) => 'onDone',
];
}
}

+ 8
- 8
src/Security/OpenStreetMapAuthenticator.php View File

@ -19,15 +19,12 @@ use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface
class OpenStreetMapAuthenticator extends OAuth2Authenticator implements AuthenticationEntrypointInterface class OpenStreetMapAuthenticator extends OAuth2Authenticator implements AuthenticationEntrypointInterface
{ {
private $clientRegistry;
private $entityManager;
private $router;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
public function __construct(
private ClientRegistry $clientRegistry,
private EntityManagerInterface $entityManager,
private RouterInterface $router,
)
{ {
$this->clientRegistry = $clientRegistry;
$this->entityManager = $entityManager;
$this->router = $router;
} }
public function supports(Request $request): ?bool public function supports(Request $request): ?bool
@ -40,6 +37,9 @@ class OpenStreetMapAuthenticator extends OAuth2Authenticator implements Authenti
$client = $this->clientRegistry->getClient('openstreetmap'); $client = $this->clientRegistry->getClient('openstreetmap');
$accessToken = $this->fetchAccessToken($client); $accessToken = $this->fetchAccessToken($client);
$session = $request->getSession();
$session->set('access_token', $accessToken);
return new SelfValidatingPassport( return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) { new UserBadge($accessToken->getToken(), function() use ($accessToken, $client) {
$resourceOwner = $client->fetchUserFromToken($accessToken); $resourceOwner = $client->fetchUserFromToken($accessToken);


+ 84
- 0
src/Service/OpenStreetMapClient.php View File

@ -0,0 +1,84 @@
<?php
namespace App\Service;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class OpenStreetMapClient {
public function __construct(
private ClientRegistry $clientRegistry,
private HttpClientInterface $client,
private RequestStack $requestStack,
) {
}
protected function query(string $method, string $path, array $params = [])
{
$session = $this->requestStack->getSession();
$accessToken = $session->get('access_token');
/*
if ($accessToken->hasExpired()) {
$accessToken = $this->clientRegistry->getClient('openstreetmap')->refreshAccessToken($accessToken->getRefreshToken());
$session->set('access_token', $accessToken);
}
*/
$token = $accessToken->getToken();
$response = $this->client->request($method, 'https://api.openstreetmap.org/api/0.6/' . $path, [
'auth_bearer' => $token,
'query' => $params,
]);
$isStatusCodeOk = ($response->getStatusCode() === 200);
if (!$isStatusCodeOk) {
throw new \RuntimeException();
}
return $response->getContent();
}
/* Cf <https://wiki.openstreetmap.org/wiki/API_v0.6#Query:_GET_/api/0.6/changesets> */
public function getChangesets(?string $username = null, ?\DateTimeInterface $since = null, ?\DateTimeInterface $then = null)
{
$changesets = [];
$params = [];
if (!is_null($username)) {
$params['display_name'] = $username;
}
if (!is_null($since) and !is_null($then)) {
$params['time'] = implode(',', [
$since->format(\DateTimeInterface::ISO8601),
$then->format(\DateTimeInterface::ISO8601),
]);
} elseif (!is_null($since) and is_null($then)) {
$params['time'] = $since->format(\DateTimeInterface::ISO8601);
}
$response = $this->query('GET', 'changesets', $params);
$xml = new \DOMDocument();
$xml->loadXML($response);
$xpath = new \DOMXPath($xml);
$changesetNodes = $xpath->query('/osm/changeset');
foreach ($changesetNodes as $changesetNode) {
$changeset = [];
foreach ($changesetNode->attributes as $attribute) {
$changeset[$attribute->name] = $attribute->value;
}
$tagNodes = $xpath->query('./tag', $changesetNode);
foreach ($tagNodes as $tagNode) {
$changeset[$tagNode->getAttribute('k')] = $tagNode->getAttribute('v');
}
$changesets[] = $changeset;
}
return $changesets;
}
}

+ 29
- 0
src/Service/OsmoseClient.php View File

@ -0,0 +1,29 @@
<?php
/*
min_lat="44.1762654" min_lon="4.0874079" max_lat="44.2089911" max_lon="4.1176352"
curl 'https://osmose.openstreetmap.fr/api/0.3/issues?bbox=4.0874079,44.1762654,4.1176352,44.2089911&username=caboulot'
curl 'https://osmose.openstreetmap.fr/api/0.3/issues?bbox=4.0874079,44.1762654,4.1176352,44.2089911'
{"issues":[{"lat":44.1794503,"lon":4.088304,"id":"b1ff426d-dcad-71ed-1d2a-6c2046bf1ba8","item":"8500"},{"lat":44.1780477,"lon":4.0904077,"id":"fbd4dc15-473c-b180-df76-270f27d20f49","item":"8100"},{"lat":44.1804446,"lon":4.0925989,"id":"b36d1a0a-2d9f-5ece-9df6-f029c4988f5c","item":"8100"},{"lat":44.176561,"lon":4.1107394,"id":"060128fb-52ee-b07b-701b-0e08c2845e2f","item":"8100"},{"lat":44.1780305,"lon":4.111765,"id":"962b9b5c-cd23-6599-97af-e826714bd1d4","item":"8100"},{"lat":44.1838506,"lon":4.1046709,"id":"9a958f77-82b0-97c9-c201-ca2f59be1c92","item":"8100"},{"lat":44.177464,"lon":4.097858,"id":"1e1c187c-c4dc-aefb-b3b5-7efde7094793","item":"1230"},{"lat":44.1851377,"lon":4.1068794,"id":"31d93adf-ea1c-8fd2-6d81-af675592c9ae","item":"8280"},{"lat":44.1852151,"lon":4.1098047,"id":"020388a0-5b51-a223-8ce2-f5bdbdafc462","item":"8280"},{"lat":44.1778859,"lon":4.1118038,"id":"3ad94aec-a112-caaf-0e05-3000b8ea713b","item":"8280"},{"lat":44.1771659,"lon":4.0946588,"id":"77adb92d-7a47-af45-a9f3-1e8bb9f77f0f","item":"8280"},{"lat":44.1792438,"lon":4.0910476,"id":"616a6997-6679-85c5-0aea-94d30842e730","item":"8260"},{"lat":44.177985,"lon":4.091316,"id":"3511f1a6-5b22-f33a-08a0-6e282c0f186d","item":"8310"},{"lat":44.179733,"lon":4.09073,"id":"23b66128-22ee-b3b6-07cb-9716778675ff","item":"7170"},{"lat":44.1983061,"lon":4.1083305,"id":"eedd870f-44b0-cc77-e2b1-b892b32a5ada","item":"8280"},{"lat":44.177167,"lon":4.105044,"id":"6adee921-9dc1-9512-f80c-c250b45f0402","item":"8310"},{"lat":44.179557,"lon":4.105813,"id":"e9b3aedf-b251-5e4a-aff2-d47303a5fe0d","item":"8310"},{"lat":44.1900522,"lon":4.0960352,"id":"e9d11fcd-83bd-307b-771d-9b134a7bfb4b","item":"2130"},{"lat":44.1882819,"lon":4.0974304,"id":"44317976-917b-e246-97ca-d0957c56396e","item":"3091"},{"lat":44.1814147,"lon":4.111102,"id":"1fd7c71a-5b33-18bc-9491-41a725c20eac","item":"5080"},{"lat":44.18067,"lon":4.11218,"id":"846eb7d2-3a6b-4a28-887f-17775482148d","item":"8170"},{"lat":44.183475,"lon":4.111835,"id":"62977de3-898f-5408-a854-d8b11374705b","item":"8310"},{"lat":44.183475,"lon":4.111835,"id":"14256577-f477-bdd7-0d59-aaf8ebf591a4","item":"8310"},{"lat":44.1808883,"lon":4.1130807,"id":"c5076142-ef71-d3b8-404e-87829c8a2328","item":"9001"},{"lat":44.1805634,"lon":4.1152882,"id":"9f6a686a-a5dc-f729-82a0-7faf48e8c428","item":"9001"},{"lat":44.1861172,"lon":4.103424,"id":"92ea4bef-1a7e-74bc-2a43-7927433095cd","item":"8530"},{"lat":44.1832749,"lon":4.1110508,"id":"75435dce-7b79-cfcb-7ecc-4df42a3cb7ab","item":"8100"},{"lat":44.176445,"lon":4.1148587,"id":"e3b91e9e-052a-c0f0-5e46-14df8c5ce01b","item":"8280"},{"lat":44.1765674,"lon":4.1074229,"id":"1a033442-2212-af3f-21b9-d1283bddc949","item":"8280"},{"lat":44.1830498,"lon":4.1095534,"id":"59842e30-7fcc-a7b0-76c7-7584848672bc","item":"8280"},{"lat":44.1870279,"lon":4.1117828,"id":"2ca1e7ad-2c95-7146-aef0-6e8df970122d","item":"8500"},{"lat":44.1772469,"lon":4.0947452,"id":"501299b6-da5b-c95e-347e-fad8cf62ab9c","item":"1210"},{"lat":44.177329,"lon":4.096523,"id":"c874c7e8-1118-e959-f050-96302b319b32","item":"8310"},{"lat":44.1866871,"lon":4.0977601,"id":"e8d04270-e146-a4d5-760a-a3a2316ee898","item":"9006"},{"lat":44.1866871,"lon":4.0977601,"id":"f154b78d-5360-f971-b736-6489c7df670e","item":"3091"},{"lat":44.1817631,"lon":4.0895483,"id":"907291b9-ac78-faf8-1ad0-f36f3d859afb","item":"8010"},{"lat":44.1791288,"lon":4.0954703,"id":"1ccdc69c-29be-48a3-232c-91c2dadaa799","item":"1230"},{"lat":44.1810253,"lon":4.1044074,"id":"aa17ed0c-b354-4c81-728c-39926fbb759d","item":"8280"},{"lat":44.1832127,"lon":4.111835,"id":"2c8ae62c-0db2-474e-f05a-e2dc6ebcee73","item":"7170"},{"lat":44.1882819,"lon":4.0974304,"id":"e6dd4e37-f745-cad9-126c-5507a733d70c","item":"9006"},{"lat":44.1793645,"lon":4.0918519,"id":"c2ad06be-da22-44a9-d902-e10c96dc686f","item":"8280"},{"lat":44.1791669,"lon":4.0927244,"id":"bec7d376-3596-8926-3cac-be61cac97f0c","item":"8100"},{"lat":44.1775655,"lon":4.0904883,"id":"502cd4b5-3330-221e-f696-e14655727126","item":"8280"},{"lat":44.1800258,"lon":4.0992066,"id":"6e74f642-2c7a-c6ee-d101-4bd6847ece68","item":"8100"},{"lat":44.1823563,"lon":4.0940009,"id":"89f6c38f-e3be-247f-f818-287c89918362","item":"8280"},{"lat":44.2045949,"lon":4.0887296,"id":"b6d4a358-fee6-454f-a8b1-9fd8f37f6456","item":"8280"}]}
issues.rss
issues.geojson
curl 'https://osmose.openstreetmap.fr/api/0.3/user/caboulot' par user
*/
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class OsmoseClient {
public function __construct(
private HttpClientInterface $client,
) {
}
}

+ 2
- 0
templates/partials/_task-locking.html.twig View File

@ -1,4 +1,5 @@
{% if workflow_metadata(task, 'locking', task.status) %} {% if workflow_metadata(task, 'locking', task.status) %}
{% if task.isLocked %}
<span class="me-2"> <span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1"/> <path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1"/>
@ -18,3 +19,4 @@
</svg> </svg>
</span> </span>
{% endif %} {% endif %}
{% endif %}

Loading…
Cancel
Save