Browse Source

finalise deux/trois bricoles pour avoir une poc fonctionnelle

master
vincent 2 months ago
parent
commit
4b13a33bc6
36 changed files with 916 additions and 108 deletions
  1. +9
    -0
      assets/app.js
  2. +34
    -7
      assets/controllers/josm_controller.js
  3. +2
    -0
      composer.json
  4. +209
    -1
      composer.lock
  5. +43
    -0
      migrations/Version20240726180444.php
  6. +51
    -0
      migrations/Version20240726191510.php
  7. +51
    -0
      migrations/Version20240726191624.php
  8. +39
    -0
      migrations/Version20240727205448.php
  9. +38
    -0
      migrations/Version20240728194704.php
  10. +6
    -4
      src/Controller/HomeController.php
  11. +53
    -22
      src/Controller/ProjectController.php
  12. +87
    -39
      src/Controller/TaskController.php
  13. +16
    -0
      src/DataFixtures/AppFixtures.php
  14. +33
    -0
      src/Entity/Comment.php
  15. +45
    -0
      src/Entity/Project.php
  16. +1
    -1
      src/Entity/Task.php
  17. +32
    -0
      src/Form/CsvType.php
  18. +7
    -0
      src/Form/ProjectType.php
  19. +3
    -3
      src/Form/TaskType.php
  20. +16
    -14
      src/Repository/CommentRepository.php
  21. +34
    -0
      src/Repository/TaskRepository.php
  22. +4
    -0
      src/Service/GeoJsonManager.php
  23. +0
    -7
      templates/_footer.html.twig
  24. +1
    -1
      templates/base.html.twig
  25. +1
    -0
      templates/comment/cancel.md.twig
  26. +1
    -0
      templates/comment/finish.md.twig
  27. +1
    -0
      templates/comment/reset.md.twig
  28. +1
    -0
      templates/comment/start.md.twig
  29. +12
    -0
      templates/partials/_comment-metadata.html.twig
  30. +2
    -2
      templates/project/create.html.twig
  31. +4
    -0
      templates/project/index.html.twig
  32. +44
    -0
      templates/project/show.html.twig
  33. +2
    -2
      templates/project/update.html.twig
  34. +2
    -2
      templates/task/create.html.twig
  35. +31
    -2
      templates/task/show.html.twig
  36. +1
    -1
      templates/task/update.html.twig

+ 9
- 0
assets/app.js View File

@ -7,3 +7,12 @@ import './styles/app.css';
import { Tooltip } from './vendor/bootstrap/bootstrap.index.js'; import { Tooltip } from './vendor/bootstrap/bootstrap.index.js';
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl)) const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl))
document.addEventListener('click', function (event) {
if (event.target.matches('a[href*="remove"]')) {
if (!confirm(event.target.innerText + ' ?')) {
event.preventDefault();
return false;
}
}
});

+ 34
- 7
assets/controllers/josm_controller.js View File

@ -2,8 +2,17 @@ import { Controller } from '@hotwired/stimulus';
export default class extends Controller { export default class extends Controller {
static values = { static values = {
imagery: String,
importurl: String, importurl: String,
layername: String, layername: String,
sendosm: Boolean,
bottom: Number,
top: Number,
left: Number,
right: Number,
comment: String,
source: String,
hashtags: String,
} }
remoteControl() { remoteControl() {
@ -18,26 +27,44 @@ export default class extends Controller {
.then(function (json) { .then(function (json) {
console.log('JOSM v' + json.version); console.log('JOSM v' + json.version);
url = baseurl + '/imagery?' + (new URLSearchParams({ url = baseurl + '/imagery?' + (new URLSearchParams({
'id': 'osmfr', // 'fr.orthohr', // cf <https://josm.openstreetmap.de/wiki/Maps>
'id': _this.imageryValue,
})); }));
fetch(url) fetch(url)
.then(function (response) { .then(function (response) {
return response.text(); return response.text();
}) })
.then(function (text) { .then(function (text) {
console.log(text);
console.log('JOSM imagery ' + text);
}); });
url = baseurl + '/import?' + (new URLSearchParams({
'new_layer': true,
'layer_name': _this.layernameValue,
'url': _this.importurlValue,
if (_this.sendosmValue) {
url = baseurl + '/import?' + (new URLSearchParams({
'new_layer': true,
'layer_name': _this.layernameValue,
'url': _this.importurlValue,
}));
fetch(url)
.then(function (response) {
return response.text();
})
.then(function (text) {
console.log('JOSM import ' + text);
});
}
url = baseurl + '/zoom?' + (new URLSearchParams({
'bottom': _this.bottomValue,
'top': _this.topValue,
'left': _this.leftValue,
'right': _this.rightValue,
'changeset_comment': _this.commentValue,
'changeset_source': _this.sourceValue,
'changeset_hashtags': _this.hashtagsValue,
})); }));
fetch(url) fetch(url)
.then(function (response) { .then(function (response) {
return response.text(); return response.text();
}) })
.then(function (text) { .then(function (text) {
console.log(text);
console.log('JOSM zoom ' + text);
}); });
}); });
} }


+ 2
- 0
composer.json View File

@ -17,6 +17,7 @@
"knpuniversity/oauth2-client-bundle": "^2.18", "knpuniversity/oauth2-client-bundle": "^2.18",
"league/commonmark": "^2.4", "league/commonmark": "^2.4",
"league/html-to-markdown": "^5.1", "league/html-to-markdown": "^5.1",
"phayes/geophp": "^1.2",
"stof/doctrine-extensions-bundle": "^1.12", "stof/doctrine-extensions-bundle": "^1.12",
"symfony/asset": "7.1.*", "symfony/asset": "7.1.*",
"symfony/asset-mapper": "7.1.*", "symfony/asset-mapper": "7.1.*",
@ -25,6 +26,7 @@
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "7.1.*", "symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*", "symfony/framework-bundle": "7.1.*",
"symfony/mime": "7.1.*",
"symfony/runtime": "7.1.*", "symfony/runtime": "7.1.*",
"symfony/security-bundle": "7.1.*", "symfony/security-bundle": "7.1.*",
"symfony/stimulus-bundle": "^2.18", "symfony/stimulus-bundle": "^2.18",


+ 209
- 1
composer.lock View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d673dbc41a6c7573a7676813f6beb0c1",
"content-hash": "705ac1a078cbdb34254dbd216eaeb8c4",
"packages": [ "packages": [
{ {
"name": "behat/transliterator", "name": "behat/transliterator",
@ -2853,6 +2853,46 @@
"time": "2020-10-15T08:29:30+00:00" "time": "2020-10-15T08:29:30+00:00"
}, },
{ {
"name": "phayes/geophp",
"version": "1.2",
"source": {
"type": "git",
"url": "https://github.com/phayes/geoPHP.git",
"reference": "015404e85b602e0df1f91441f8db0f9e98f7e567"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phayes/geoPHP/zipball/015404e85b602e0df1f91441f8db0f9e98f7e567",
"reference": "015404e85b602e0df1f91441f8db0f9e98f7e567",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "4.1.*"
},
"type": "library",
"autoload": {
"classmap": [
"geoPHP.inc"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2 or New-BSD"
],
"authors": [
{
"name": "Patrick Hayes"
}
],
"description": "GeoPHP is a open-source native PHP library for doing geometry operations. It is written entirely in PHP and can therefore run on shared hosts. It can read and write a wide variety of formats: WKT (including EWKT), WKB (including EWKB), GeoJSON, KML, GPX, GeoRSS). It works with all Simple-Feature geometries (Point, LineString, Polygon, GeometryCollection etc.) and can be used to get centroids, bounding-boxes, area, and a wide variety of other useful information.",
"homepage": "https://github.com/phayes/geoPHP",
"support": {
"issues": "https://github.com/phayes/geoPHP/issues",
"source": "https://github.com/phayes/geoPHP/tree/master"
},
"time": "2014-12-02T06:11:22+00:00"
},
{
"name": "psr/cache", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",
"source": { "source": {
@ -5398,6 +5438,90 @@
"time": "2024-05-31T14:57:53+00:00" "time": "2024-05-31T14:57:53+00:00"
}, },
{ {
"name": "symfony/mime",
"version": "v7.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
"reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/serializer": "^6.4.3|^7.0.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.1.2"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-28T10:03:55+00:00"
},
{
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v7.1.1", "version": "v7.1.1",
"source": { "source": {
@ -5699,6 +5823,90 @@
"time": "2024-05-31T15:07:36+00:00" "time": "2024-05-31T15:07:36+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-idn",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c",
"shasum": ""
},
"require": {
"php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.30.0", "version": "v1.30.0",
"source": { "source": {


+ 43
- 0
migrations/Version20240726180444.php View File

@ -0,0 +1,43 @@
<?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 Version20240726180444 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__comment AS SELECT id, task_id, content FROM comment');
$this->addSql('DROP TABLE comment');
$this->addSql('CREATE TABLE comment (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, task_id INTEGER NOT NULL, created_by_id INTEGER NOT NULL, content CLOB NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_9474526C8DB60186 FOREIGN KEY (task_id) REFERENCES task (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_9474526CB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO comment (id, task_id, content) SELECT id, task_id, content FROM __temp__comment');
$this->addSql('DROP TABLE __temp__comment');
$this->addSql('CREATE INDEX IDX_9474526C8DB60186 ON comment (task_id)');
$this->addSql('CREATE INDEX IDX_9474526CB03A8386 ON comment (created_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__comment AS SELECT id, task_id, content FROM comment');
$this->addSql('DROP TABLE comment');
$this->addSql('CREATE TABLE comment (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, task_id INTEGER NOT NULL, content CLOB NOT NULL, CONSTRAINT FK_9474526C8DB60186 FOREIGN KEY (task_id) REFERENCES task (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO comment (id, task_id, content) SELECT id, task_id, content FROM __temp__comment');
$this->addSql('DROP TABLE __temp__comment');
$this->addSql('CREATE INDEX IDX_9474526C8DB60186 ON comment (task_id)');
}
}

+ 51
- 0
migrations/Version20240726191510.php View File

@ -0,0 +1,51 @@
<?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 Version20240726191510 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, 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 \'\' NOT 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) 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)');
$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)');
}
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 NOT 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)');
}
}

+ 51
- 0
migrations/Version20240726191624.php View File

@ -0,0 +1,51 @@
<?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 Version20240726191624 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, 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) 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)');
$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)');
}
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 \'\' NOT 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)');
}
}

+ 39
- 0
migrations/Version20240727205448.php View File

@ -0,0 +1,39 @@
<?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 Version20240727205448 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 project ADD COLUMN hashtags VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE project ADD COLUMN source VARCHAR(255) 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__project AS SELECT id, created_by_id, name, description, slug, created_at FROM project');
$this->addSql('DROP TABLE project');
$this->addSql('CREATE TABLE project (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_by_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, slug VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_2FB3D0EEB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO project (id, created_by_id, name, description, slug, created_at) SELECT id, created_by_id, name, description, slug, created_at FROM __temp__project');
$this->addSql('DROP TABLE __temp__project');
$this->addSql('CREATE UNIQUE INDEX UNIQ_2FB3D0EE989D9B62 ON project (slug)');
$this->addSql('CREATE INDEX IDX_2FB3D0EEB03A8386 ON project (created_by_id)');
}
}

+ 38
- 0
migrations/Version20240728194704.php View File

@ -0,0 +1,38 @@
<?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 Version20240728194704 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 project ADD COLUMN imagery VARCHAR(255) 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__project AS SELECT id, created_by_id, name, description, slug, created_at, hashtags, source FROM project');
$this->addSql('DROP TABLE project');
$this->addSql('CREATE TABLE project (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_by_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, slug VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, hashtags VARCHAR(255) DEFAULT NULL, source VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_2FB3D0EEB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO project (id, created_by_id, name, description, slug, created_at, hashtags, source) SELECT id, created_by_id, name, description, slug, created_at, hashtags, source FROM __temp__project');
$this->addSql('DROP TABLE __temp__project');
$this->addSql('CREATE UNIQUE INDEX UNIQ_2FB3D0EE989D9B62 ON project (slug)');
$this->addSql('CREATE INDEX IDX_2FB3D0EEB03A8386 ON project (created_by_id)');
}
}

+ 6
- 4
src/Controller/HomeController.php View File

@ -34,11 +34,13 @@ class HomeController extends AbstractController
{ {
$client = $clientRegistry->getClient('openstreetmap'); $client = $clientRegistry->getClient('openstreetmap');
try {
$resourceOwner = $client->fetchUser();
if ($request->query->has('error')) {
$this->addFlash('danger', sprintf(
'Échec de l’authentification (%s)',
$request->query->has('error_description') ? $request->query->get('error_description') : $request->query->get('error')
));
} else {
$this->addFlash('success', 'Authentification OSM réussie !'); $this->addFlash('success', 'Authentification OSM réussie !');
} catch (IdentityProviderException $exception) {
$this->addFlash('danger', 'Échec de l’authentification OSM !');
} }
return $this->redirectToRoute('app_home'); return $this->redirectToRoute('app_home');


+ 53
- 22
src/Controller/ProjectController.php View File

@ -2,8 +2,10 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Comment;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\Task; use App\Entity\Task;
use App\Form\CsvType;
use App\Form\ProjectType; use App\Form\ProjectType;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -21,11 +23,6 @@ class ProjectController extends AbstractController
{ {
$projects = $entityManager->getRepository(Project::class)->findAll(); $projects = $entityManager->getRepository(Project::class)->findAll();
$this->addFlash(
'info',
sprintf('%s projet%s trouvé%s', count($projects), (count($projects) > 1 ? 's' : ''), (count($projects) > 1 ? 's' : ''))
);
return $this->render('project/index.html.twig', [ return $this->render('project/index.html.twig', [
'projects' => $projects, 'projects' => $projects,
]); ]);
@ -72,29 +69,68 @@ class ProjectController extends AbstractController
public function show(EntityManagerInterface $entityManager, $slug): Response public function show(EntityManagerInterface $entityManager, $slug): Response
{ {
$project = $entityManager->getRepository(Project::class)->findOneBySlug($slug); $project = $entityManager->getRepository(Project::class)->findOneBySlug($slug);
$tasks = $entityManager->getRepository(Task::class)->findByProjectPaginated($project);
if (!$project) { if (!$project) {
$this->addFlash(
'warning',
'Projet non trouvé !'
);
$this->addFlash('warning', 'Projet non trouvé !');
return $this->redirectToRoute('app_project'); return $this->redirectToRoute('app_project');
} }
$count = count($project->getTasks());
$this->addFlash(
'info',
sprintf('%s tâche%s trouvée%s', $count, ($count > 1 ? 's' : ''), ($count > 1 ? 's' : ''))
);
$tasks = $entityManager->getRepository(Task::class)->findByProjectPaginated($project);
$randomTask = $entityManager->getRepository(Task::class)->findRandomByProject($project, Task::STATUS_TODO);
$csvForm = $this->createForm(CsvType::class, null, [
'action' => $this->generateUrl('app_project_import', ['slug' => $slug])
]);
$csvForm->add('submit', SubmitType::class, ['label' => 'Importer']);
$comments = $entityManager->getRepository(Comment::class)->findLatestByProject($project);
return $this->render('project/show.html.twig', [ return $this->render('project/show.html.twig', [
'project' => $project, 'project' => $project,
'tasks' => $tasks, 'tasks' => $tasks,
'csv_form' => $csvForm,
'comments' => $comments,
'randomTask' => $randomTask,
]); ]);
} }
#[Route('/{slug}/import', name: 'app_project_import')]
public function import(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$project = $entityManager->getRepository(Project::class)->findOneBySlug($slug);
if (!$project) {
$this->addFlash('warning', 'Projet non trouvé !');
return $this->redirectToRoute('app_project');
}
$csvForm = $this->createForm(CsvType::class, null, [
'allow_extra_fields' => true,
]);
$csvForm->handleRequest($request);
if ($csvForm->isSubmitted() and $csvForm->isValid()) {
$csvFile = $csvForm->get('csv')->getData();
$csv = fopen($csvFile->getPathName(), 'r');
$col = array_flip(fgetcsv($csv));
while ($row = fgetcsv($csv)) {
$task = new Task();
$task->setCreatedBy($this->getUser());
$task->setProject($project);
$task->setName($row[$col['name']]);
$task->setDescription($row[$col['description']]);
$task->setOsm($row[$col['osm']]);
$task->setGeojson(json_decode($row[$col['geojson']], true));
$task->setStatus(Task::STATUS_TODO);
$entityManager->persist($task);
}
$entityManager->flush();
}
return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
}
#[Route('/{slug}/update', name: 'app_project_update')] #[Route('/{slug}/update', name: 'app_project_update')]
public function update(Request $request, EntityManagerInterface $entityManager, $slug): Response public function update(Request $request, EntityManagerInterface $entityManager, $slug): Response
{ {
@ -128,7 +164,7 @@ class ProjectController extends AbstractController
'Projet modifié !' 'Projet modifié !'
); );
return $this->redirectToRoute('app_project');
return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash( $this->addFlash(
'danger', 'danger',
@ -162,11 +198,6 @@ class ProjectController extends AbstractController
$entityManager->remove($project); $entityManager->remove($project);
$entityManager->flush(); $entityManager->flush();
$this->addFlash(
'success',
'Projet supprimé !'
);
return $this->redirectToRoute('app_project'); return $this->redirectToRoute('app_project');
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash( $this->addFlash(


+ 87
- 39
src/Controller/TaskController.php View File

@ -11,6 +11,7 @@ use App\Service\GeoJsonManager;
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;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\JsonResponse; 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;
@ -21,17 +22,20 @@ use Symfony\Component\Workflow\WorkflowInterface;
class TaskController extends AbstractController class TaskController extends AbstractController
{ {
#[Route('/create', name: 'app_task_create')] #[Route('/create', name: 'app_task_create')]
public function create(Request $request, EntityManagerInterface $entityManager, $slug): Response
public function create(Request $request, EntityManagerInterface $entityManager): Response
{ {
if (!$request->query->has('slug')) {
$this->addFlash( 'warning', 'Projet non spécifié !');
return $this->redirectToRoute('app_project');
}
$slug = $request->query->get('slug');
$repository = $entityManager->getRepository(Project::class); $repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($slug); $project = $repository->findOneBySlug($slug);
if (!$project) { if (!$project) {
$this->addFlash(
'warning',
'Projet non trouvé !'
);
$this->addFlash( 'warning', 'Projet non trouvé !');
return $this->redirectToRoute('app_project'); return $this->redirectToRoute('app_project');
} }
@ -58,10 +62,7 @@ class TaskController extends AbstractController
return $this->redirectToRoute('app_project_show', ['slug' => $slug]); return $this->redirectToRoute('app_project_show', ['slug' => $slug]);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash(
'danger',
'Impossible de créer la tâche !'
);
$this->addFlash('danger', 'Impossible de créer la tâche !');
} }
} }
@ -84,9 +85,14 @@ class TaskController extends AbstractController
'Tâche non trouvée !' 'Tâche non trouvée !'
); );
if (!$request->headers->has('Referer')) {
throw $this->createNotFoundException('Task not found');
}
return $this->redirect($request->headers->get('Referer')); return $this->redirect($request->headers->get('Referer'));
} }
$nextTask = $repository->findNextOne($task);
$comment = new Comment(); $comment = new Comment();
$commentForm = $this->createForm(CommentType::class, $comment, [ $commentForm = $this->createForm(CommentType::class, $comment, [
'action' => $this->generateUrl('app_task_comment', ['slug' => $slug]), 'action' => $this->generateUrl('app_task_comment', ['slug' => $slug]),
@ -95,10 +101,15 @@ class TaskController extends AbstractController
'label' => 'Commenter', 'label' => 'Commenter',
]); ]);
$geom = \geoPHP::load($task->getGeojson(), 'json');
$bbox = $geom->getBBox();
return $this->render('task/show.html.twig', [ return $this->render('task/show.html.twig', [
'task' => $task, 'task' => $task,
'project' => $task->getProject(), 'project' => $task->getProject(),
'commentForm' => $commentForm, 'commentForm' => $commentForm,
'nextTask' => $nextTask,
'bbox' => $bbox,
]); ]);
} }
@ -118,6 +129,7 @@ class TaskController extends AbstractController
} }
$comment = new Comment(); $comment = new Comment();
$comment->setCreatedBy($this->getUser());
$comment->setTask($task); $comment->setTask($task);
$commentForm = $this->createForm(CommentType::class, $comment); $commentForm = $this->createForm(CommentType::class, $comment);
$commentForm->add('submit', SubmitType::class, [ $commentForm->add('submit', SubmitType::class, [
@ -132,14 +144,10 @@ class TaskController extends AbstractController
$entityManager->persist($comment); $entityManager->persist($comment);
$entityManager->flush(); $entityManager->flush();
$this->addFlash(
'success',
'Commentaire ajouté !'
);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash( $this->addFlash(
'danger', 'danger',
'Impossible de commenter !'
'Impossible de commenter ! ' . $exception->getMessage()
); );
} }
} }
@ -175,11 +183,6 @@ class TaskController extends AbstractController
$entityManager->persist($task); $entityManager->persist($task);
$entityManager->flush(); $entityManager->flush();
$this->addFlash(
'success',
'Tâche modifiée !'
);
return $this->redirectToRoute('app_task_show', ['slug' => $slug]); return $this->redirectToRoute('app_task_show', ['slug' => $slug]);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash( $this->addFlash(
@ -217,11 +220,6 @@ class TaskController extends AbstractController
$entityManager->remove($task); $entityManager->remove($task);
$entityManager->flush(); $entityManager->flush();
$this->addFlash(
'success',
'Tâche supprimée !'
);
return $this->redirectToRoute('app_project_show', ['slug' => $project->getSlug()]); return $this->redirectToRoute('app_project_show', ['slug' => $project->getSlug()]);
} catch (\Exception $exception) { } catch (\Exception $exception) {
$this->addFlash( $this->addFlash(
@ -233,7 +231,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): Response
private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $slug, $transitionName, $commentTemplate): Response
{ {
$repository = $entityManager->getRepository(Task::class); $repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($slug); $task = $repository->findOneBySlug($slug);
@ -265,14 +263,18 @@ class TaskController extends AbstractController
} elseif ($shouldTransitionUnlock) { } elseif ($shouldTransitionUnlock) {
$task->unlock(); $task->unlock();
} }
$entityManager->persist($task); $entityManager->persist($task);
$entityManager->flush();
$this->addFlash(
'success',
'La tâche est modifiée !'
);
$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();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->addFlash( $this->addFlash(
'warning', 'warning',
@ -286,28 +288,28 @@ 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);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_START, 'comment/start.md.twig');
} }
#[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);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_FINISH, 'comment/finish.md.twig');
} }
#[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);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_CANCEL, 'comment/cancel.md.twig');
} }
#[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);
return $this->transition($taskLifecycleStateMachine, $entityManager, $slug, Task::TRANSITION_RESET, 'comment/reset.md.twig');
} }
#[Route('/{slug}.geojson', name: 'app_task_geojson')]
#[Route('/download/{slug}.geojson', name: 'app_task_geojson')]
public function geojson(Request $request, EntityManagerInterface $entityManager, $slug): Response public function geojson(Request $request, EntityManagerInterface $entityManager, $slug): Response
{ {
$repository = $entityManager->getRepository(Task::class); $repository = $entityManager->getRepository(Task::class);
@ -318,13 +320,52 @@ class TaskController extends AbstractController
'warning', 'warning',
'Tâche non trouvée !' 'Tâche non trouvée !'
); );
// TODO faire pareil ailleurs où il y a des referer
if (!$request->headers->has('Referer')) {
throw $this->createNotFoundException('Task not found');
}
return $this->redirect($request->headers->get('Referer'));
}
$response = JsonResponse::fromJsonString($task->getGeojson());
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
sprintf('%s.geojson', $task->getSlug())
));
return $response;
}
#[Route('/download/{slug}.gpx', name: 'app_task_gpx')]
public function gpx(Request $request, EntityManagerInterface $entityManager, $slug): Response
{
$repository = $entityManager->getRepository(Task::class);
$task = $repository->findOneBySlug($slug);
if (!$task) {
$this->addFlash('warning', 'Tâche non trouvée !');
if (!$request->headers->has('Referer')) {
throw $this->createNotFoundException('Task not found');
}
return $this->redirect($request->headers->get('Referer')); return $this->redirect($request->headers->get('Referer'));
} }
return JsonResponse::fromJsonString($task->getGeojson());
$geom = \geoPHP::load($task->getGeojson(), 'json');
$gpx = $geom->out('gpx');
$response = new Response($gpx);
$response->headers->set('Content-Type', 'application/xml');
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
sprintf('%s.gpx', $task->getSlug())
));
return $response;
} }
#[Route('/{slug}.osm', name: 'app_task_osm')]
#[Route('/download/{slug}.osm', name: 'app_task_osm')]
public function osm(Request $request, EntityManagerInterface $entityManager, $slug): Response public function osm(Request $request, EntityManagerInterface $entityManager, $slug): Response
{ {
$repository = $entityManager->getRepository(Task::class); $repository = $entityManager->getRepository(Task::class);
@ -340,8 +381,15 @@ class TaskController extends AbstractController
$xml = new \DOMDocument(); $xml = new \DOMDocument();
$xml->loadXml($task->getOsm()); $xml->loadXml($task->getOsm());
$response = new Response($xml->saveXML()); $response = new Response($xml->saveXML());
$response->headers->set('Content-Type', 'application/xml'); $response->headers->set('Content-Type', 'application/xml');
$response->headers->set('Content-Disposition', HeaderUtils::makeDisposition(
HeaderUtils::DISPOSITION_ATTACHMENT,
sprintf('%s.osm', $task->getSlug())
));
return $response; return $response;
} }


+ 16
- 0
src/DataFixtures/AppFixtures.php View File

@ -13,6 +13,22 @@ class AppFixtures extends Fixture
{ {
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
$user = new User();
$user->setUsername('Ptigrouick');
$user->setOsmId(195175);
$manager->persist($user);
$user = new User();
$user->setUsername('pyrog');
$user->setOsmId(270456);
$manager->persist($user);
$user = new User();
$user->setUsername('AdrienHegy');
$user->setOsmId(19054951);
$manager->persist($user);
$user = new User(); $user = new User();
$user->setUsername('caboulot'); $user->setUsername('caboulot');
$user->setOsmId(80638); $user->setOsmId(80638);


+ 33
- 0
src/Entity/Comment.php View File

@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\CommentRepository; use App\Repository\CommentRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
#[ORM\Entity(repositoryClass: CommentRepository::class)] #[ORM\Entity(repositoryClass: CommentRepository::class)]
class Comment class Comment
@ -21,6 +22,14 @@ class Comment
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: false)]
private ?Task $task = null; private ?Task $task = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?User $createdBy = null;
#[ORM\Column]
#[Gedmo\Timestampable(on: 'create')]
private ?\DateTimeImmutable $createdAt = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -49,4 +58,28 @@ class Comment
return $this; return $this;
} }
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): static
{
$this->createdBy = $createdBy;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
} }

+ 45
- 0
src/Entity/Project.php View File

@ -47,6 +47,15 @@ class Project
#[Gedmo\Timestampable(on: 'create')] #[Gedmo\Timestampable(on: 'create')]
private ?\DateTimeImmutable $createdAt = null; private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $hashtags = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $source = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $imagery = null;
public function __construct() public function __construct()
{ {
$this->tags = new ArrayCollection(); $this->tags = new ArrayCollection();
@ -165,4 +174,40 @@ class Project
return $this; return $this;
} }
public function getHashtags(): ?string
{
return $this->hashtags;
}
public function setHashtags(?string $hashtags): static
{
$this->hashtags = $hashtags;
return $this;
}
public function getSource(): ?string
{
return $this->source;
}
public function setSource(?string $source): static
{
$this->source = $source;
return $this;
}
public function getImagery(): ?string
{
return $this->imagery;
}
public function setImagery(?string $imagery): static
{
$this->imagery = $imagery;
return $this;
}
} }

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

@ -49,7 +49,7 @@ class Task
#[ORM\Column(type: Types::TEXT)] #[ORM\Column(type: Types::TEXT)]
private ?string $geojson = null; private ?string $geojson = null;
#[ORM\Column(type: Types::TEXT)]
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $osm = null; private ?string $osm = null;
/** /**


+ 32
- 0
src/Form/CsvType.php View File

@ -0,0 +1,32 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
class CsvType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('csv', FileType::class, [
'mapped' => false,
'label' => 'Fichier CSV',
'constraints' => [
new File([
'maxSize' => '10M',
'mimeTypes' => [
'text/csv',
'text/plain',
],
'mimeTypesMessage' => 'Type MIME inattendu',
])
],
])
;
}
}

+ 7
- 0
src/Form/ProjectType.php View File

@ -16,6 +16,13 @@ class ProjectType extends AbstractType
$builder $builder
->add('name', null, ['label' => 'Nom']) ->add('name', null, ['label' => 'Nom'])
->add('description', null, ['label' => 'Description']) ->add('description', null, ['label' => 'Description'])
->add('hashtags', null, ['label' => 'Hashtags'])
->add('source', null, ['label' => 'Source'])
->add('imagery', null, [
'label' => 'Imagerie',
'help_html' => true,
'help' => 'Cf <a href="https://josm.openstreetmap.de/wiki/Maps" target="_blank">le wiki</a>',
])
->add('tags', EntityType::class, [ ->add('tags', EntityType::class, [
'label' => 'Étiquettes', 'label' => 'Étiquettes',
'class' => Tag::class, 'class' => Tag::class,


+ 3
- 3
src/Form/TaskType.php View File

@ -17,10 +17,10 @@ class TaskType extends AbstractType
->add('name', null, ['label' => 'Nom']) ->add('name', null, ['label' => 'Nom'])
->add('description', null, ['label' => 'Description']) ->add('description', null, ['label' => 'Description'])
->add('geojson', TextareaType::class, ['label' => 'GeoJSON']) ->add('geojson', TextareaType::class, ['label' => 'GeoJSON'])
->add('osm', TextareaType::class, ['label' => 'OSM'])
->add('osm', TextareaType::class, ['label' => 'OSM', 'required' => false])
->add('status', TaskLifecycleType::class, ['label' => 'État']) ->add('status', TaskLifecycleType::class, ['label' => 'État'])
->add('urgent', null, ['label' => 'Urgence'])
->add('important', null, ['label' => 'Importance'])
->add('urgent', null, ['label' => 'Urgence', 'required' => false])
->add('important', null, ['label' => 'Importance', 'required' => false])
; ;
} }


+ 16
- 14
src/Repository/CommentRepository.php View File

@ -2,6 +2,8 @@
namespace App\Repository; namespace App\Repository;
use App\Entity\Task;
use App\Entity\Project;
use App\Entity\Comment; use App\Entity\Comment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
@ -16,20 +18,20 @@ class CommentRepository extends ServiceEntityRepository
parent::__construct($registry, Comment::class); parent::__construct($registry, Comment::class);
} }
// /**
// * @return Comment[] Returns an array of Comment objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
/**
* @return Comment[] Returns an array of Comment objects
*/
public function findLatestByProject(Project $project): array
{
return $this->createQueryBuilder('c')
->join(Task::class, 't')
->andWhere('t.project = :project')
->setParameter('project', $project)
->orderBy('c.createdAt', 'ASC')
->getQuery()
->getResult()
;
}
// public function findOneBySomeField($value): ?Comment // public function findOneBySomeField($value): ?Comment
// { // {


+ 34
- 0
src/Repository/TaskRepository.php View File

@ -65,4 +65,38 @@ class TaskRepository extends ServiceEntityRepository
); );
} }
public function findNextOne(Task $task, $order = 'ASC')
{
return $this->createQueryBuilder('t')
->andWhere('t.project = :project')
->andWhere('t.id > :id')
->setParameter('project', $task->getProject())
->setParameter('id', $task->getId())
->orderBy('t.id', $order)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult()
;
}
public function findRandomByProject(Project $project, $status = null)
{
$qb = $this->createQueryBuilder('t')
->andWhere('t.project = :project')
->setParameter('project', $project);
if (!is_null($status)) {
$qb->andWhere('t.status = :status')
->setParameter('status', $status);
}
$tasks = $qb->getQuery()
->getResult()
;
shuffle($tasks);
return reset($tasks);
}
} }

+ 4
- 0
src/Service/GeoJsonManager.php View File

@ -20,11 +20,15 @@ class GeoJsonManager
private function getFullGeoJson(Task $task) private function getFullGeoJson(Task $task)
{ {
$data = json_decode($task->getGeojson(), true); $data = json_decode($task->getGeojson(), true);
if (isset($data['features'][0]['properties'])) {
$data['features'][0]['properties'] = array_merge($data['features'][0]['properties'], [ $data['features'][0]['properties'] = array_merge($data['features'][0]['properties'], [
'name' => $task->getName(), 'name' => $task->getName(),
'url' => $this->router->generate('app_task_show', ['slug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL), 'url' => $this->router->generate('app_task_show', ['slug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL),
'color' => $this->taskLifecycleStateMachine->getMetadataStore()->getPlaceMetadata($task->getStatus())['color'], 'color' => $this->taskLifecycleStateMachine->getMetadataStore()->getPlaceMetadata($task->getStatus())['color'],
]); ]);
} else {
dump($data);
}
return $data; return $data;
} }


+ 0
- 7
templates/_footer.html.twig View File

@ -1,7 +0,0 @@
<div class="container">
<div class="row bg-body-tertiary border-top p-2">
<div class="col">
<p class="text-muted mb-0"></p>
</div>
</div>
</div>

+ 1
- 1
templates/base.html.twig View File

@ -45,7 +45,7 @@
{% endblock %} {% endblock %}
</div> </div>
</main> </main>
<footer>{% block footer %}{% include '_footer.html.twig' %}{% endblock %}</footer>
{% endblock %} {% endblock %}
{% block modals %}{% endblock %}
</body> </body>
</html> </html>

+ 1
- 0
templates/comment/cancel.md.twig View File

@ -0,0 +1 @@
{{ user.username }} abandonne la tâche [{{ task.name }}]({{ path('app_task_show', {'slug': task.slug}) }})

+ 1
- 0
templates/comment/finish.md.twig View File

@ -0,0 +1 @@
{{ user.username }} termine la tâche [{{ task.name }}]({{ path('app_task_show', {'slug': task.slug}) }})

+ 1
- 0
templates/comment/reset.md.twig View File

@ -0,0 +1 @@
{{ user.username }} recommence la tâche [{{ task.name }}]({{ path('app_task_show', {'slug': task.slug}) }})

+ 1
- 0
templates/comment/start.md.twig View File

@ -0,0 +1 @@
{{ user.username }} commence la tâche [{{ task.name }}]({{ path('app_task_show', {'slug': task.slug}) }})

+ 12
- 0
templates/partials/_comment-metadata.html.twig View File

@ -0,0 +1,12 @@
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
</svg>
{{ comment.createdBy.username }}
</span>
<span class="me-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
</svg>
{{ comment.createdAt|format_datetime('short', 'short', locale='fr') }}
</span>

+ 2
- 2
templates/project/create.html.twig View File

@ -2,10 +2,10 @@
{% block breadcrumb %} {% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li> <li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_create') }}">Créer un nouveau projet</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_create') }}">Créer un projet</a></li>
{% endblock %} {% endblock %}
{% block page_title %}Créer un nouveau projet{% endblock %}
{% block page_title %}Créer un projet{% endblock %}
{% block page_content %} {% block page_content %}
{{ form(create_form) }} {{ form(create_form) }}


+ 4
- 0
templates/project/index.html.twig View File

@ -37,5 +37,9 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
{% if not is_granted('IS_AUTHENTICATED_FULLY') %}
<p>Il n’y a pas encore de projet. Connectez-vous pour pouvoir en créer un.</p>
{% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

+ 44
- 0
templates/project/show.html.twig View File

@ -23,8 +23,12 @@
<a href="{{ path('app_project_update', {'slug': project.slug}) }}" class="btn btn-secondary">Modifier le projet</a> <a href="{{ path('app_project_update', {'slug': project.slug}) }}" class="btn btn-secondary">Modifier le projet</a>
<a href="{{ path('app_project_remove', {'slug': project.slug}) }}" class="btn btn-secondary">Supprimer le projet</a> <a href="{{ path('app_project_remove', {'slug': project.slug}) }}" class="btn btn-secondary">Supprimer le projet</a>
{% endif %} {% endif %}
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#modal">Importer des tâches</button>
<a href="{{ path('app_task_create', {'slug': project.slug}) }}" class="btn btn-secondary">Créer une tâche</a> <a href="{{ path('app_task_create', {'slug': project.slug}) }}" class="btn btn-secondary">Créer une tâche</a>
{% endif %} {% endif %}
{% if randomTask %}
<a href="{{ path('app_task_show', {'slug': randomTask.slug}) }}" class="btn btn-secondary">Piocher une tâche</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -41,12 +45,14 @@
</div> </div>
{% endif %} {% endif %}
{% if project.tasks is not empty %}
<h2 class="mb-3">Carte</h2> <h2 class="mb-3">Carte</h2>
<div class="row"> <div class="row">
<div class="col mb-3"> <div class="col mb-3">
{{ macro.map(project) }} {{ macro.map(project) }}
</div> </div>
</div> </div>
{% endif %}
{% if project.tasks is not empty %} {% if project.tasks is not empty %}
{% set dummy = tasks.setParam('_fragment', 'tasks') %} {% set dummy = tasks.setParam('_fragment', 'tasks') %}
@ -89,4 +95,42 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if comments is not empty %}
<h2 class="mb-3">Commentaires</h2>
<div class="row">
<div class="col mb-3">
{% for comment in comments %}
<blockquote class="blockquote">
{{ comment.content|markdown_to_html }}
</blockquote>
<figcaption class="blockquote-footer">
{% include 'partials/_comment-metadata.html.twig' %}
</figcaption>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock %}
{% block modals %}
{{ form_start(csv_form) }}
<div class="modal fade" id="modal" tabindex="-1" aria-labelledby="modalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalLabel">Importer des tâches</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ form_row(csv_form.csv) }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
{{ form_widget(csv_form.submit) }}
</div>
</div>
</div>
</div>
{{ form_end(csv_form) }}
{% endblock %} {% endblock %}

+ 2
- 2
templates/project/update.html.twig View File

@ -2,10 +2,10 @@
{% block breadcrumb %} {% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li> <li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_create') }}">Modifier le projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_create') }}">{{ project.name }}</a></li>
{% endblock %} {% endblock %}
{% block page_title %}Modifier le projet {{ project.name }}{% endblock %}
{% block page_title %}Modifier le projet{% endblock %}
{% block page_content %} {% block page_content %}
{{ form(update_form) }} {{ form(update_form) }}


+ 2
- 2
templates/task/create.html.twig View File

@ -3,10 +3,10 @@
{% block breadcrumb %} {% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li> <li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">Projet {{ project.name }}</a></li> <li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_create', {'slug': project.slug}) }}">Créer une nouvelle tâche</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_create', {'slug': project.slug}) }}">Créer une tâche</a></li>
{% endblock %} {% endblock %}
{% block page_title %}Créer une nouvelle tâche{% endblock %}
{% block page_title %}Créer une tâche{% endblock %}
{% block page_content %} {% block page_content %}
{{ form(create_form) }} {{ form(create_form) }}


+ 31
- 2
templates/task/show.html.twig View File

@ -32,11 +32,32 @@
Télécharger la tâche Télécharger la tâche
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ path('app_task_geojson', {'slug': task.slug}) }}" target="_blank">GeoJSON</a></li>
<li><a class="dropdown-item" href="{{ path('app_task_geojson', {'slug': task.slug}) }}">.geojson</a></li>
<li><a class="dropdown-item" href="{{ path('app_task_gpx', {'slug': task.slug}) }}">.gpx</a></li>
</ul> </ul>
</div> </div>
<button class="btn btn-secondary" type="button" data-controller="josm" data-action="click->josm#remoteControl" data-josm-importurl-value="{{ url('app_task_osm', {'slug': task.slug}) }}" data-josm-layername-value="{{ task.name }}">Télécommande JOSM</button>
<button
class="btn btn-secondary"
type="button"
data-controller="josm"
data-action="click->josm#remoteControl"
data-josm-imagery-value="{{ project.imagery is not empty ? project.imagery : 'osmfr' }}"
data-josm-importurl-value="{{ url('app_task_osm', {'slug': task.slug}) }}"
data-josm-layername-value="{{ task.name }}"
data-josm-sendosm-value="{{ task.osm is not empty }}"
data-josm-bottom-value="{{ bbox.miny }}" {# Minimum latitude 43.923196020314 #}
data-josm-top-value="{{ bbox.maxy }}" {# Maximum latitude 43.94450663651 #}
data-josm-left-value="{{ bbox.minx }}" {# Minimum longitude 4.3220213108728 #}
data-josm-right-value="{{ bbox.maxx }}" {# Maximum longitude 4.37742111766 #}
data-josm-comment-value="{{ project.name }} {{ task.name }}"
data-josm-source-value="{{ project.source }}"
data-josm-hashtags-value="{{ project.hashtags }}"
>Télécommande JOSM</button>
{% endif %}
{% endif %} {% endif %}
{% if nextTask %}
<a href="{{ path('app_task_show', {'slug': nextTask.slug}) }}" class="btn btn-secondary">Tâche suivante</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -66,7 +87,9 @@
</div> </div>
{% if not (task.comments is empty or not is_granted('IS_AUTHENTICATED_FULLY')) %}
<h2 class="mb-3">Commentaires</h2> <h2 class="mb-3">Commentaires</h2>
{% endif %}
{% if task.comments is not empty %} {% if task.comments is not empty %}
<div class="row"> <div class="row">
<div class="col mb-3"> <div class="col mb-3">
@ -74,13 +97,19 @@
<blockquote class="blockquote"> <blockquote class="blockquote">
{{ comment.content|markdown_to_html }} {{ comment.content|markdown_to_html }}
</blockquote> </blockquote>
<figcaption class="blockquote-footer">
{% include 'partials/_comment-metadata.html.twig' %}
</figcaption>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<div class="row"> <div class="row">
<div class="col mb-3"> <div class="col mb-3">
{{ form(commentForm) }} {{ form(commentForm) }}
</div> </div>
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

+ 1
- 1
templates/task/update.html.twig View File

@ -6,7 +6,7 @@
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'slug': task.slug}) }}">{{ task.name }}</a></li> <li class="breadcrumb-item"><a href="{{ path('app_task_show', {'slug': task.slug}) }}">{{ task.name }}</a></li>
{% endblock %} {% endblock %}
{% block page_title %}Modifier la tâche {{ task.name }}{% endblock %}
{% block page_title %}Modifier la tâche{% endblock %}
{% block page_content %} {% block page_content %}
{{ form(update_form) }} {{ form(update_form) }}


Loading…
Cancel
Save