From 89028c74cba74279d9b39e9800b9ab849cf55922 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 9 Jul 2021 23:24:12 -0300 Subject: [PATCH 01/21] Command to import from Trello Signed-off-by: Vitor Mattos --- lib/Command/BoardImport.php | 424 +++++++++++++++++++++++ lib/Command/fixtures/setting-schema.json | 17 + 2 files changed, 441 insertions(+) create mode 100644 lib/Command/BoardImport.php create mode 100644 lib/Command/fixtures/setting-schema.json diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php new file mode 100644 index 000000000..296c08bf8 --- /dev/null +++ b/lib/Command/BoardImport.php @@ -0,0 +1,424 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command; + +use JsonSchema\Validator; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Stack; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\LabelService; +use OCA\Deck\Service\PermissionService; +use OCA\Deck\Service\StackService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\IGroupManager; +use OCP\IUserManager; +use OCP\IUserSession; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; + +class BoardImport extends Command { + /** @var BoardService */ + private $boardService; + // protected $cardMapper; + /** @var LabelService */ + private $labelService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var IUserManager */ + private $userManager; + // /** @var IGroupManager */ + // private $groupManager; + // private $assignedUsersMapper; + private $allowedSystems = ['trello']; + /** @var Board */ + private $board; + + public function __construct( + // BoardMapper $boardMapper, + BoardService $boardService, + LabelService $labelService, + StackMapper $stackMapper, + CardMapper $cardMapper, + // IUserSession $userSession, + // StackMapper $stackMapper, + // CardMapper $cardMapper, + // AssignmentMapper $assignedUsersMapper, + IUserManager $userManager + // IGroupManager $groupManager + ) { + parent::__construct(); + + // $this->cardMapper = $cardMapper; + $this->boardService = $boardService; + $this->labelService = $labelService; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + + // $this->userSession = $userSession; + // $this->stackMapper = $stackMapper; + // $this->assignedUsersMapper = $assignedUsersMapper; + // $this->boardMapper = $boardMapper; + + $this->userManager = $userManager; + // $this->groupManager = $groupManager; + } + + protected function configure() { + $this + ->setName('deck:import') + ->setDescription('Import data') + ->addOption( + 'system', + null, + InputOption::VALUE_REQUIRED, + 'Source system for import. Available options: trello.', + 'trello' + ) + ->addOption( + 'setting', + null, + InputOption::VALUE_REQUIRED, + 'Configuration json file.', + 'config.json' + ) + ->addOption( + 'data', + null, + InputOption::VALUE_REQUIRED, + 'Data file to import.', + 'data.json' + ) + ; + } + + /** + * @inheritDoc + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + $this->validateSystem($input, $output); + $this->validateData($input, $output); + $this->validateSettings($input, $output); + $this->validateUsers(); + $this->validateOwner(); + } + + public function validateData(InputInterface $input, OutputInterface $output) { + $filename = $input->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($input, $output, $question); + $input->setOption('data', $data); + } + $this->data = json_decode(file_get_contents($filename)); + if (!$this->data) { + $output->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($input, $output); + } + if (!$this->data) { + $this->data = json_decode(file_get_contents($filename)); + } + } + + private function validateOwner() { + $this->settings->owner = $this->userManager->get($this->settings->owner); + if (!$this->settings->owner) { + throw new \LogicException('Owner "' . $this->settings->owner . '" not found on Nextcloud. Check setting json.'); + } + } + + private function validateUsers() { + if (empty($this->settings->uidRelation)) { + return; + } + foreach ($this->settings->uidRelation as $trelloUid => $nextcloudUid) { + $user = array_filter($this->data->members, fn($u) => $u->username === $trelloUid); + if (!$user) { + throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); + } + if (!is_string($nextcloudUid)) { + throw new \LogicException('User on setting uidRelation must be a string'); + } + $this->settings->uidRelation->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->settings->uidRelation->$trelloUid) { + throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); + } + } + } + + private function validateSystem(InputInterface $input, OutputInterface $output) { + if (in_array($input->getOption('system'), $this->allowedSystems)) { + return; + } + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Please inform a source system', + $this->allowedSystems, + 0 + ); + $question->setErrorMessage('System %s is invalid.'); + $system = $helper->ask($input, $output, $question); + $input->setOption('system', $system); + } + + private function validateSettings(InputInterface $input, OutputInterface $output) { + if (!is_file($input->getOption('setting'))) { + $helper = $this->getHelper('question'); + $question = new Question( + 'Please inform a valid setting json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Setting file not found' + ); + } + return $answer; + }); + $setting = $helper->ask($input, $output, $question); + $input->setOption('setting', $setting); + } + + $this->settings = json_decode(file_get_contents($input->getOption('setting'))); + $validator = new Validator(); + $validator->validate( + $this->settings, + (object)['$ref' => 'file://' . realpath(__DIR__ . '/fixtures/setting-schema.json')] + ); + if (!$validator->isValid()) { + $output->writeln('Invalid setting file'); + $output->writeln(array_map(fn($v) => $v['message'], $validator->getErrors())); + $output->writeln('Valid schema:'); + $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); + $input->setOption('setting', null); + $this->validateSettings($input, $output); + } + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws \ReflectionException + */ + protected function execute(InputInterface $input, OutputInterface $output) { + // $this->boardService->setUserId($this->settings->owner->getUID()); + $this->setUserId($this->settings->owner->getUID()); + // $this->userSession->setUser($this->settings->owner); + $this->importBoard(); + $this->importLabels(); + $this->importStacks(); + $this->importCards(); + // $boards = $this->boardService->findAll(); + + // $data = []; + // foreach ($boards as $board) { + // $fullBoard = $this->boardMapper->find($board->getId(), true, true); + // $data[$board->getId()] = (array)$fullBoard->jsonSerialize(); + // $stacks = $this->stackMapper->findAll($board->getId()); + // foreach ($stacks as $stack) { + // $data[$board->getId()]['stacks'][] = (array)$stack->jsonSerialize(); + // $cards = $this->cardMapper->findAllByStack($stack->getId()); + // foreach ($cards as $card) { + // $fullCard = $this->cardMapper->find($card->getId()); + // $assignedUsers = $this->assignedUsersMapper->findAll($card->getId()); + // $fullCard->setAssignedUsers($assignedUsers); + // $data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = (array)$fullCard->jsonSerialize(); + // } + // } + // } + // $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); + return self::SUCCESS; + } + + private function checklistItem($item) { + if (($item->state == 'incomplete')) { + $string_start = '- [ ]'; + } else { + $string_start = '- [x]'; + } + $check_item_string = $string_start . ' ' . $item->name . "\n"; + return $check_item_string; + } + + function formulateChecklistText($checklist) { + $checklist_string = "\n\n## {$checklist->name}\n"; + foreach ($checklist->checkItems as $item) { + $checklist_item_string = $this->checklistItem($item); + $checklist_string = $checklist_string . "\n" . $checklist_item_string; + } + return $checklist_string; + } + + private function importCards() { + # Save checklist content into a dictionary (_should_ work even if a card has multiple checklists + foreach ($this->data->checklists as $checklist) { + $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); + } + $this->data->checklists = $checklists; + + foreach ($this->data->cards as $trelloCard) { + # Check whether a card is archived, if true, skipping to the next card + if ($trelloCard->closed) { + continue; + } + if ((count($trelloCard->idChecklists) !== 0)) { + foreach ($this->data->checklists[$trelloCard->id] as $checklist) { + $trelloCard->desc .= "\n" . $checklist; + } + } + + $card = new Card(); + $card->setTitle($trelloCard->name); + $card->setStackId($this->stacks[$trelloCard->idList]); + $card->setType('plain'); + $card->setOrder($trelloCard->idShort); + $card->setOwner($this->settings->owner->getUID()); + $card->setDescription($trelloCard->desc); + if ($trelloCard->due) { + $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.000\Z', $trelloCard->due) + ->format('Y-m-d H:i:s'); + $card->setDuedate($duedate); + } + $card = $this->cardMapper->insert($card); + + $this->associateCardToLabels($card->getId(), $trelloCard); + } + } + + public function associateCardToLabels($cardId, $card) { + foreach ($card->labels as $label) { + $this->cardMapper->assignLabel( + $cardId, + $this->labels[$label->id]->getId() + ); + } + } + + private function importStacks() { + $this->stacks = []; + foreach ($this->data->lists as $order => $list) { + if ($list->closed) { + continue; + } + $stack = new Stack(); + $stack->setTitle($list->name); + $stack->setBoardId($this->board->getId()); + $stack->setOrder($order + 1); + $stack = $this->stackMapper->insert($stack); + $this->stacks[$list->id] = $stack; + } + } + + private function translateColor($color) { + switch ($color) { + case 'red': + return 'ff0000'; + case 'yellow': + return 'ffff00'; + case 'orange': + return 'ff6600'; + case 'green': + return '00ff00'; + case 'purple': + return '9900ff'; + case 'blue': + return '0000ff'; + case 'sky': + return '00ccff'; + case 'lime': + return '00ff99'; + case 'pink': + return 'ff66cc'; + case 'black': + return '000000'; + default: + return 'ffffff'; + } + } + + private function importBoard() { + $this->board = $this->boardService->create( + $this->data->name, + $this->settings->owner->getUID(), + $this->settings->color + ); + // $this->boardService->find($this->board->getId()); + } + + public function importLabels() { + $this->labels = []; + foreach ($this->data->labels as $label) { + if (empty($label->name)) { + $labelTitle = 'Unnamed ' . $label->color . ' label'; + } else { + $labelTitle = $label->name; + } + $newLabel = $this->labelService->create( + $labelTitle, + $this->translateColor($label->color), + $this->board->getId() + ); + $this->labels[$label->id] = $newLabel; + } + } + + private function setUserId() { + $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); + $propertyPermissionService->setAccessible(true); + $permissionService = $propertyPermissionService->getValue($this->labelService); + + $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); + $propertyUserId->setAccessible(true); + $propertyUserId->setValue($permissionService, $this->settings->owner->getUID()); + } +} diff --git a/lib/Command/fixtures/setting-schema.json b/lib/Command/fixtures/setting-schema.json new file mode 100644 index 000000000..b21501c2b --- /dev/null +++ b/lib/Command/fixtures/setting-schema.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "uidRelation": { + "type": "object" + }, + "owner": { + "type": "string", + "required": true + }, + "color": { + "type": "string", + "required": true, + "pattern": "^[0-9a-fA-F]{6}$" + } + } +} \ No newline at end of file From 48df98ce671ed8edbe7221badde1086f8aac388e Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 9 Jul 2021 23:35:56 -0300 Subject: [PATCH 02/21] Add new command Clean code Add new command Import last modified and deleted date Replace arrow functions by lambda functions Add properties to class Add dependency to composer.json Signed-off-by: Vitor Mattos Turn private methods Add output messages and associate users to cards Signed-off-by: Vitor Mattos --- appinfo/info.xml | 1 + composer.json | 3 +- composer.lock | 120 +++- lib/Command/BoardImport.php | 376 ++--------- lib/Command/Helper/ImportAbstract.php | 86 +++ lib/Command/Helper/ImportInterface.php | 47 ++ lib/Command/Helper/TrelloHelper.php | 429 +++++++++++++ lib/Command/UserExport.php | 3 + lib/Command/fixtures/setting-schema.json | 3 + tests/unit/Command/BoardImportTest.php | 77 +++ .../unit/Command/Helper/TrelloHelperTest.php | 134 ++++ tests/unit/Command/fixtures/data-trello.json | 582 ++++++++++++++++++ .../unit/Command/fixtures/setting-trello.json | 7 + 13 files changed, 1513 insertions(+), 355 deletions(-) create mode 100644 lib/Command/Helper/ImportAbstract.php create mode 100644 lib/Command/Helper/ImportInterface.php create mode 100644 lib/Command/Helper/TrelloHelper.php create mode 100644 tests/unit/Command/BoardImportTest.php create mode 100644 tests/unit/Command/Helper/TrelloHelperTest.php create mode 100644 tests/unit/Command/fixtures/data-trello.json create mode 100644 tests/unit/Command/fixtures/setting-trello.json diff --git a/appinfo/info.xml b/appinfo/info.xml index cf8d707e2..0bf8183e6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -44,6 +44,7 @@ OCA\Deck\Command\UserExport + OCA\Deck\Command\BoardImport diff --git a/composer.json b/composer.json index 7bf463588..88d40fc46 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ } ], "require": { - "cogpowered/finediff": "0.3.*" + "cogpowered/finediff": "0.3.*", + "justinrainbow/json-schema": "^5.2" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index 08f9378c0..64256a599 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f6d91406db4e7e16e31113951986e13", + "content-hash": "cf4fb1b424f5f0c36ecc1391b10de59c", "packages": [ { "name": "cogpowered/finediff", @@ -60,6 +60,76 @@ "source": "https://github.com/cogpowered/FineDiff/tree/master" }, "time": "2014-05-19T10:25:02+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.11", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11" + }, + "time": "2021-07-22T09:24:00+00:00" } ], "packages-dev": [ @@ -3677,16 +3747,16 @@ }, { "name": "symfony/console", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4" + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", + "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", "shasum": "" }, "require": { @@ -3756,7 +3826,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.1" + "source": "https://github.com/symfony/console/tree/v5.4.2" }, "funding": [ { @@ -3772,7 +3842,7 @@ "type": "tidelift" } ], - "time": "2021-12-09T11:22:43+00:00" + "time": "2021-12-20T16:11:12+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4070,16 +4140,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d2f29dac98e96a98be467627bd49c2efb1bc2590" + "reference": "e77046c252be48c48a40816187ed527703c8f76c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d2f29dac98e96a98be467627bd49c2efb1bc2590", - "reference": "d2f29dac98e96a98be467627bd49c2efb1bc2590", + "url": "https://api.github.com/repos/symfony/finder/zipball/e77046c252be48c48a40816187ed527703c8f76c", + "reference": "e77046c252be48c48a40816187ed527703c8f76c", "shasum": "" }, "require": { @@ -4113,7 +4183,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.0" + "source": "https://github.com/symfony/finder/tree/v5.4.2" }, "funding": [ { @@ -4129,7 +4199,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-15T11:06:13+00:00" }, { "name": "symfony/options-resolver", @@ -4767,16 +4837,16 @@ }, { "name": "symfony/process", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5be20b3830f726e019162b26223110c8f47cf274" + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5be20b3830f726e019162b26223110c8f47cf274", - "reference": "5be20b3830f726e019162b26223110c8f47cf274", + "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", "shasum": "" }, "require": { @@ -4809,7 +4879,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.0" + "source": "https://github.com/symfony/process/tree/v5.4.2" }, "funding": [ { @@ -4825,7 +4895,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-27T21:01:00+00:00" }, { "name": "symfony/service-contracts", @@ -4974,16 +5044,16 @@ }, { "name": "symfony/string", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d" + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", + "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", "shasum": "" }, "require": { @@ -5040,7 +5110,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.0" + "source": "https://github.com/symfony/string/tree/v5.4.2" }, "funding": [ { @@ -5056,7 +5126,7 @@ "type": "tidelift" } ], - "time": "2021-11-24T10:02:00+00:00" + "time": "2021-12-16T21:52:00+00:00" }, { "name": "theseer/tokenizer", diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 296c08bf8..0870c52a1 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -1,8 +1,8 @@ + * @copyright Copyright (c) 2021 Vitor Mattos * - * @author Julius Härtl + * @author Vitor Mattos * * @license GNU AGPL version 3 or any later version * @@ -23,80 +23,37 @@ namespace OCA\Deck\Command; -use JsonSchema\Validator; -use OCA\Deck\Db\AssignmentMapper; -use OCA\Deck\Db\Board; -use OCA\Deck\Db\BoardMapper; -use OCA\Deck\Db\Card; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\Stack; -use OCA\Deck\Db\StackMapper; -use OCA\Deck\Service\BoardService; -use OCA\Deck\Service\LabelService; -use OCA\Deck\Service\PermissionService; -use OCA\Deck\Service\StackService; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\IGroupManager; -use OCP\IUserManager; -use OCP\IUserSession; +use OCA\Deck\Command\Helper\ImportInterface; +use OCA\Deck\Command\Helper\TrelloHelper; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; class BoardImport extends Command { - /** @var BoardService */ - private $boardService; - // protected $cardMapper; - /** @var LabelService */ - private $labelService; - /** @var StackMapper */ - private $stackMapper; - /** @var CardMapper */ - private $cardMapper; - /** @var IUserManager */ - private $userManager; - // /** @var IGroupManager */ - // private $groupManager; - // private $assignedUsersMapper; + /** @var string */ + private $system; private $allowedSystems = ['trello']; - /** @var Board */ - private $board; + /** @var TrelloHelper */ + private $trelloHelper; + /** + * Data object created from settings JSON + * + * @var \StdClass + */ + public $settings; public function __construct( - // BoardMapper $boardMapper, - BoardService $boardService, - LabelService $labelService, - StackMapper $stackMapper, - CardMapper $cardMapper, - // IUserSession $userSession, - // StackMapper $stackMapper, - // CardMapper $cardMapper, - // AssignmentMapper $assignedUsersMapper, - IUserManager $userManager - // IGroupManager $groupManager + TrelloHelper $trelloHelper ) { parent::__construct(); - - // $this->cardMapper = $cardMapper; - $this->boardService = $boardService; - $this->labelService = $labelService; - $this->stackMapper = $stackMapper; - $this->cardMapper = $cardMapper; - - // $this->userSession = $userSession; - // $this->stackMapper = $stackMapper; - // $this->assignedUsersMapper = $assignedUsersMapper; - // $this->boardMapper = $boardMapper; - - $this->userManager = $userManager; - // $this->groupManager = $groupManager; + $this->trelloHelper = $trelloHelper; } + /** + * @return void + */ protected function configure() { $this ->setName('deck:import') @@ -127,73 +84,35 @@ class BoardImport extends Command { /** * @inheritDoc + * + * @return void */ - protected function interact(InputInterface $input, OutputInterface $output) - { + protected function interact(InputInterface $input, OutputInterface $output) { $this->validateSystem($input, $output); - $this->validateData($input, $output); - $this->validateSettings($input, $output); - $this->validateUsers(); - $this->validateOwner(); + $this->getSystem() + ->validate($input, $output); } - public function validateData(InputInterface $input, OutputInterface $output) { - $filename = $input->getOption('data'); - if (!is_file($filename)) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($input, $output, $question); - $input->setOption('data', $data); - } - $this->data = json_decode(file_get_contents($filename)); - if (!$this->data) { - $output->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($input, $output); - } - if (!$this->data) { - $this->data = json_decode(file_get_contents($filename)); - } + private function setSystem(string $system): void { + $this->system = $system; } - private function validateOwner() { - $this->settings->owner = $this->userManager->get($this->settings->owner); - if (!$this->settings->owner) { - throw new \LogicException('Owner "' . $this->settings->owner . '" not found on Nextcloud. Check setting json.'); - } - } - - private function validateUsers() { - if (empty($this->settings->uidRelation)) { - return; - } - foreach ($this->settings->uidRelation as $trelloUid => $nextcloudUid) { - $user = array_filter($this->data->members, fn($u) => $u->username === $trelloUid); - if (!$user) { - throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); - } - if (!is_string($nextcloudUid)) { - throw new \LogicException('User on setting uidRelation must be a string'); - } - $this->settings->uidRelation->$trelloUid = $this->userManager->get($nextcloudUid); - if (!$this->settings->uidRelation->$trelloUid) { - throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); - } - } + /** + * @return ImportInterface + */ + private function getSystem() { + $helper = $this->{$this->system . 'Helper'}; + $helper->setCommand($this); + return $helper; } + /** + * @return void + */ private function validateSystem(InputInterface $input, OutputInterface $output) { - if (in_array($input->getOption('system'), $this->allowedSystems)) { + $system = $input->getOption('system'); + if (in_array($system, $this->allowedSystems)) { + $this->setSystem($system); return; } $helper = $this->getHelper('question'); @@ -205,220 +124,19 @@ class BoardImport extends Command { $question->setErrorMessage('System %s is invalid.'); $system = $helper->ask($input, $output, $question); $input->setOption('system', $system); - } - - private function validateSettings(InputInterface $input, OutputInterface $output) { - if (!is_file($input->getOption('setting'))) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid setting json file: ', - 'config.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Setting file not found' - ); - } - return $answer; - }); - $setting = $helper->ask($input, $output, $question); - $input->setOption('setting', $setting); - } - - $this->settings = json_decode(file_get_contents($input->getOption('setting'))); - $validator = new Validator(); - $validator->validate( - $this->settings, - (object)['$ref' => 'file://' . realpath(__DIR__ . '/fixtures/setting-schema.json')] - ); - if (!$validator->isValid()) { - $output->writeln('Invalid setting file'); - $output->writeln(array_map(fn($v) => $v['message'], $validator->getErrors())); - $output->writeln('Valid schema:'); - $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); - $input->setOption('setting', null); - $this->validateSettings($input, $output); - } + $this->setSystem($system); } /** * @param InputInterface $input * @param OutputInterface $output - * @return void - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - * @throws \ReflectionException + * + * @return int */ - protected function execute(InputInterface $input, OutputInterface $output) { - // $this->boardService->setUserId($this->settings->owner->getUID()); - $this->setUserId($this->settings->owner->getUID()); - // $this->userSession->setUser($this->settings->owner); - $this->importBoard(); - $this->importLabels(); - $this->importStacks(); - $this->importCards(); - // $boards = $this->boardService->findAll(); - - // $data = []; - // foreach ($boards as $board) { - // $fullBoard = $this->boardMapper->find($board->getId(), true, true); - // $data[$board->getId()] = (array)$fullBoard->jsonSerialize(); - // $stacks = $this->stackMapper->findAll($board->getId()); - // foreach ($stacks as $stack) { - // $data[$board->getId()]['stacks'][] = (array)$stack->jsonSerialize(); - // $cards = $this->cardMapper->findAllByStack($stack->getId()); - // foreach ($cards as $card) { - // $fullCard = $this->cardMapper->find($card->getId()); - // $assignedUsers = $this->assignedUsersMapper->findAll($card->getId()); - // $fullCard->setAssignedUsers($assignedUsers); - // $data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = (array)$fullCard->jsonSerialize(); - // } - // } - // } - // $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); - return self::SUCCESS; - } - - private function checklistItem($item) { - if (($item->state == 'incomplete')) { - $string_start = '- [ ]'; - } else { - $string_start = '- [x]'; - } - $check_item_string = $string_start . ' ' . $item->name . "\n"; - return $check_item_string; - } - - function formulateChecklistText($checklist) { - $checklist_string = "\n\n## {$checklist->name}\n"; - foreach ($checklist->checkItems as $item) { - $checklist_item_string = $this->checklistItem($item); - $checklist_string = $checklist_string . "\n" . $checklist_item_string; - } - return $checklist_string; - } - - private function importCards() { - # Save checklist content into a dictionary (_should_ work even if a card has multiple checklists - foreach ($this->data->checklists as $checklist) { - $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); - } - $this->data->checklists = $checklists; - - foreach ($this->data->cards as $trelloCard) { - # Check whether a card is archived, if true, skipping to the next card - if ($trelloCard->closed) { - continue; - } - if ((count($trelloCard->idChecklists) !== 0)) { - foreach ($this->data->checklists[$trelloCard->id] as $checklist) { - $trelloCard->desc .= "\n" . $checklist; - } - } - - $card = new Card(); - $card->setTitle($trelloCard->name); - $card->setStackId($this->stacks[$trelloCard->idList]); - $card->setType('plain'); - $card->setOrder($trelloCard->idShort); - $card->setOwner($this->settings->owner->getUID()); - $card->setDescription($trelloCard->desc); - if ($trelloCard->due) { - $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.000\Z', $trelloCard->due) - ->format('Y-m-d H:i:s'); - $card->setDuedate($duedate); - } - $card = $this->cardMapper->insert($card); - - $this->associateCardToLabels($card->getId(), $trelloCard); - } - } - - public function associateCardToLabels($cardId, $card) { - foreach ($card->labels as $label) { - $this->cardMapper->assignLabel( - $cardId, - $this->labels[$label->id]->getId() - ); - } - } - - private function importStacks() { - $this->stacks = []; - foreach ($this->data->lists as $order => $list) { - if ($list->closed) { - continue; - } - $stack = new Stack(); - $stack->setTitle($list->name); - $stack->setBoardId($this->board->getId()); - $stack->setOrder($order + 1); - $stack = $this->stackMapper->insert($stack); - $this->stacks[$list->id] = $stack; - } - } - - private function translateColor($color) { - switch ($color) { - case 'red': - return 'ff0000'; - case 'yellow': - return 'ffff00'; - case 'orange': - return 'ff6600'; - case 'green': - return '00ff00'; - case 'purple': - return '9900ff'; - case 'blue': - return '0000ff'; - case 'sky': - return '00ccff'; - case 'lime': - return '00ff99'; - case 'pink': - return 'ff66cc'; - case 'black': - return '000000'; - default: - return 'ffffff'; - } - } - - private function importBoard() { - $this->board = $this->boardService->create( - $this->data->name, - $this->settings->owner->getUID(), - $this->settings->color - ); - // $this->boardService->find($this->board->getId()); - } - - public function importLabels() { - $this->labels = []; - foreach ($this->data->labels as $label) { - if (empty($label->name)) { - $labelTitle = 'Unnamed ' . $label->color . ' label'; - } else { - $labelTitle = $label->name; - } - $newLabel = $this->labelService->create( - $labelTitle, - $this->translateColor($label->color), - $this->board->getId() - ); - $this->labels[$label->id] = $newLabel; - } - } - - private function setUserId() { - $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); - $propertyPermissionService->setAccessible(true); - $permissionService = $propertyPermissionService->getValue($this->labelService); - - $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); - $propertyUserId->setAccessible(true); - $propertyUserId->setValue($permissionService, $this->settings->owner->getUID()); + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->getSystem() + ->import($input, $output); + $output->writeln('Done!'); + return 0; } } diff --git a/lib/Command/Helper/ImportAbstract.php b/lib/Command/Helper/ImportAbstract.php new file mode 100644 index 000000000..de271f2c8 --- /dev/null +++ b/lib/Command/Helper/ImportAbstract.php @@ -0,0 +1,86 @@ +command = $command; + } + + /** + * @return Command + */ + public function getCommand() { + return $this->command; + } + + /** + * Get a setting + * + * @param string $setting Setting name + * @return mixed + */ + public function getSetting($setting) { + return $this->settings->$setting; + } + + /** + * Define a setting + * + * @param string $settingName + * @param mixed $value + * @return void + */ + public function setSetting($settingName, $value) { + $this->settings->$settingName = $value; + } + + protected function validateSettings(InputInterface $input, OutputInterface $output): void { + $settingFile = $input->getOption('setting'); + if (!is_file($settingFile)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid setting json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Setting file not found' + ); + } + return $answer; + }); + $settingFile = $helper->ask($input, $output, $question); + $input->setOption('setting', $settingFile); + } + + $this->settings = json_decode(file_get_contents($settingFile)); + $validator = new Validator(); + $validator->validate( + $this->settings, + (object)['$ref' => 'file://' . realpath(__DIR__ . '/../fixtures/setting-schema.json')] + ); + if (!$validator->isValid()) { + $output->writeln('Invalid setting file'); + $output->writeln(array_map(function ($v) { + return $v['message']; + }, $validator->getErrors())); + $output->writeln('Valid schema:'); + $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); + $input->setOption('setting', null); + $this->validateSettings($input, $output); + } + } +} diff --git a/lib/Command/Helper/ImportInterface.php b/lib/Command/Helper/ImportInterface.php new file mode 100644 index 000000000..08f0e6a68 --- /dev/null +++ b/lib/Command/Helper/ImportInterface.php @@ -0,0 +1,47 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command\Helper; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +interface ImportInterface { + /** + * Validate data before run execute method + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + public function validate(InputInterface $input, OutputInterface $output): void; + + /** + * Run import + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + public function import(InputInterface $input, OutputInterface $output): void; +} diff --git a/lib/Command/Helper/TrelloHelper.php b/lib/Command/Helper/TrelloHelper.php new file mode 100644 index 000000000..50e1be346 --- /dev/null +++ b/lib/Command/Helper/TrelloHelper.php @@ -0,0 +1,429 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command\Helper; + +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Assignment; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Label; +use OCA\Deck\Db\Stack; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\LabelService; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class TrelloHelper extends ImportAbstract implements ImportInterface { + /** @var BoardService */ + private $boardService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var AssignmentMapper */ + private $assignmentMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var IDBConnection */ + private $connection; + /** @var IUserManager */ + private $userManager; + /** @var TrelloActions */ + private $trelloActions; + /** @var Board */ + private $board; + /** @var LabelService */ + private $labelService; + /** + * Data object created from JSON of origin system + * + * @var \StdClass + */ + private $data; + /** + * Array of stacks + * + * @var Stack[] + */ + private $stacks = []; + /** + * Array of labels + * + * @var Label[] + */ + private $labels = []; + /** @var Card[] */ + private $cards = []; + /** @var IUser */ + private $members = []; + + public function __construct( + BoardService $boardService, + LabelService $labelService, + StackMapper $stackMapper, + CardMapper $cardMapper, + AssignmentMapper $assignmentMapper, + AclMapper $aclMapper, + IDBConnection $connection, + IUserManager $userManager + ) { + $this->boardService = $boardService; + $this->labelService = $labelService; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + $this->assignmentMapper = $assignmentMapper; + $this->aclMapper = $aclMapper; + $this->connection = $connection; + $this->userManager = $userManager; + } + + public function validate(InputInterface $input, OutputInterface $output): void { + $this->validateData($input, $output); + $this->validateSettings($input, $output); + $this->validateUsers(); + $this->validateOwner(); + } + + public function import(InputInterface $input, OutputInterface $output): void { + $this->setUserId(); + $output->writeln('Importing board...'); + $this->importBoard(); + $output->writeln('Assign users to board...'); + $this->assignUsersToBoard(); + $output->writeln('Importing labels...'); + $this->importLabels(); + $output->writeln('Importing stacks...'); + $this->importStacks(); + $output->writeln('Importing cards...'); + $this->importCards(); + } + + private function assignUsersToBoard() { + foreach ($this->members as $member) { + $acl = new Acl(); + $acl->setBoardId($this->board->getId()); + $acl->setType(Acl::PERMISSION_TYPE_USER); + $acl->setParticipant($member->getUid()); + $acl->setPermissionEdit(true); + $acl->setPermissionShare($member->getUID() === $this->getSetting('owner')->getUID()); + $acl->setPermissionManage($member->getUID() === $this->getSetting('owner')->getUID()); + $this->aclMapper->insert($acl); + } + } + + private function validateData(InputInterface $input, OutputInterface $output): void { + $filename = $input->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($input, $output, $question); + $input->setOption('data', $data); + } + $this->data = json_decode(file_get_contents($filename)); + if (!$this->data) { + $output->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($input, $output); + } + } + + private function validateOwner(): void { + $owner = $this->userManager->get($this->getSetting('owner')); + if (!$owner) { + throw new \LogicException('Owner "' . $this->getSetting('owner') . '" not found on Nextcloud. Check setting json.'); + } + $this->setSetting('owner', $owner); + } + + /** + * @return void + */ + private function validateUsers() { + if (empty($this->getSetting('uidRelation'))) { + return; + } + foreach ($this->getSetting('uidRelation') as $trelloUid => $nextcloudUid) { + $user = array_filter($this->data->members, function ($u) use ($trelloUid) { + return $u->username === $trelloUid; + }); + if (!$user) { + throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); + } + if (!is_string($nextcloudUid)) { + throw new \LogicException('User on setting uidRelation must be a string'); + } + $this->getSetting('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->getSetting('uidRelation')->$trelloUid) { + throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); + } + $user = current($user); + $this->members[$user->id] = $this->getSetting('uidRelation')->$trelloUid; + } + } + + private function checklistItem($item): string { + if (($item->state == 'incomplete')) { + $string_start = '- [ ]'; + } else { + $string_start = '- [x]'; + } + $check_item_string = $string_start . ' ' . $item->name . "\n"; + return $check_item_string; + } + + private function formulateChecklistText($checklist): string { + $checklist_string = "\n\n## {$checklist->name}\n"; + foreach ($checklist->checkItems as $item) { + $checklist_item_string = $this->checklistItem($item); + $checklist_string = $checklist_string . "\n" . $checklist_item_string; + } + return $checklist_string; + } + + private function importCards(): void { + $checklists = []; + foreach ($this->data->checklists as $checklist) { + $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); + } + $this->data->checklists = $checklists; + + foreach ($this->data->cards as $trelloCard) { + $card = new Card(); + $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); + $card->setLastModified($lastModified->format('Y-m-d H:i:s')); + if ($trelloCard->closed) { + $card->setDeletedAt($lastModified->format('U')); + } + if ((count($trelloCard->idChecklists) !== 0)) { + foreach ($this->data->checklists[$trelloCard->id] as $checklist) { + $trelloCard->desc .= "\n" . $checklist; + } + } + $this->appendAttachmentsToDescription($trelloCard); + + $card->setTitle($trelloCard->name); + $card->setStackId($this->stacks[$trelloCard->idList]->getId()); + $card->setType('plain'); + $card->setOrder($trelloCard->idShort); + $card->setOwner($this->getSetting('owner')->getUID()); + $card->setDescription($trelloCard->desc); + if ($trelloCard->due) { + $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) + ->format('Y-m-d H:i:s'); + $card->setDuedate($duedate); + } + $card = $this->cardMapper->insert($card); + $this->cards[$trelloCard->id] = $card; + + $this->associateCardToLabels($card, $trelloCard); + $this->importComments($card, $trelloCard); + $this->assignToMember($card, $trelloCard); + } + } + + private function appendAttachmentsToDescription($trelloCard) { + if (empty($trelloCard->attachments)) { + return; + } + $translations = $this->getSetting('translations'); + $attachmentsLabel = empty($translations->{'Attachments'}) ? 'Attachments' : $translations->{'Attachments'}; + $URLLabel = empty($translations->{'URL'}) ? 'URL' : $translations->{'URL'}; + $nameLabel = empty($translations->{'Name'}) ? 'Name' : $translations->{'Name'}; + $dateLabel = empty($translations->{'Date'}) ? 'Date' : $translations->{'Date'}; + $trelloCard->desc .= "\n\n## {$attachmentsLabel}\n"; + $trelloCard->desc .= "| $URLLabel | $nameLabel | $dateLabel |\n"; + $trelloCard->desc .= "|---|---|---|\n"; + foreach ($trelloCard->attachments as $attachment) { + $name = $attachment->name === $attachment->url ? null : $attachment->name; + $trelloCard->desc .= "| {$attachment->url} | {$name} | {$attachment->date} |\n"; + } + } + + private function assignToMember(Card $card, $trelloCard) { + foreach ($trelloCard->idMembers as $idMember) { + $assignment = new Assignment(); + $assignment->setCardId($card->getId()); + $assignment->setParticipant($this->members[$idMember]->getUID()); + $assignment->setType(Assignment::TYPE_USER); + $assignment = $this->assignmentMapper->insert($assignment); + } + } + + private function importComments(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { + $comments = array_filter( + $this->data->actions, + function ($a) use ($trelloCard) { + return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + } + ); + foreach ($comments as $trelloComment) { + if (!empty($this->getSetting('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getSetting('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + } else { + $actor = $this->getSetting('owner')->getUID(); + } + $message = $this->replaceUsernames($trelloComment->data->text); + $qb = $this->connection->getQueryBuilder(); + + $values = [ + 'parent_id' => $qb->createNamedParameter(0), + 'topmost_parent_id' => $qb->createNamedParameter(0), + 'children_count' => $qb->createNamedParameter(0), + 'actor_type' => $qb->createNamedParameter('users'), + 'actor_id' => $qb->createNamedParameter($actor), + 'message' => $qb->createNamedParameter($message), + 'verb' => $qb->createNamedParameter('comment'), + 'creation_timestamp' => $qb->createNamedParameter( + \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) + ->format('Y-m-d H:i:s') + ), + 'latest_child_timestamp' => $qb->createNamedParameter(null), + 'object_type' => $qb->createNamedParameter('deckCard'), + 'object_id' => $qb->createNamedParameter($card->getId()), + ]; + + $qb->insert('comments') + ->values($values) + ->execute(); + } + } + + private function replaceUsernames($text) { + foreach ($this->getSetting('uidRelation') as $trello => $nextcloud) { + $text = str_replace($trello, $nextcloud->getUID(), $text); + } + return $text; + } + + private function associateCardToLabels(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { + foreach ($trelloCard->labels as $label) { + $this->cardMapper->assignLabel( + $card->getId(), + $this->labels[$label->id]->getId() + ); + } + } + + private function importStacks(): void { + $this->stacks = []; + foreach ($this->data->lists as $order => $list) { + $stack = new Stack(); + if ($list->closed) { + $stack->setDeletedAt(time()); + } + $stack->setTitle($list->name); + $stack->setBoardId($this->board->getId()); + $stack->setOrder($order + 1); + $stack = $this->stackMapper->insert($stack); + $this->stacks[$list->id] = $stack; + } + } + + private function translateColor($color): string { + switch ($color) { + case 'red': + return 'ff0000'; + case 'yellow': + return 'ffff00'; + case 'orange': + return 'ff6600'; + case 'green': + return '00ff00'; + case 'purple': + return '9900ff'; + case 'blue': + return '0000ff'; + case 'sky': + return '00ccff'; + case 'lime': + return '00ff99'; + case 'pink': + return 'ff66cc'; + case 'black': + return '000000'; + default: + return 'ffffff'; + } + } + + private function importBoard(): void { + $this->board = $this->boardService->create( + $this->data->name, + $this->getSetting('owner')->getUID(), + $this->getSetting('color') + ); + } + + private function importLabels(): void { + $this->labels = []; + foreach ($this->data->labels as $label) { + if (empty($label->name)) { + $labelTitle = 'Unnamed ' . $label->color . ' label'; + } else { + $labelTitle = $label->name; + } + $newLabel = $this->labelService->create( + $labelTitle, + $this->translateColor($label->color), + $this->board->getId() + ); + $this->labels[$label->id] = $newLabel; + } + } + + private function setUserId(): void { + if (!property_exists($this->labelService, 'permissionService')) { + return; + } + $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); + $propertyPermissionService->setAccessible(true); + $permissionService = $propertyPermissionService->getValue($this->labelService); + + if (!property_exists($permissionService, 'userId')) { + return; + } + + $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); + $propertyUserId->setAccessible(true); + $propertyUserId->setValue($permissionService, $this->getSetting('owner')->getUID()); + } +} diff --git a/lib/Command/UserExport.php b/lib/Command/UserExport.php index 1d73e8dcf..0b595c773 100644 --- a/lib/Command/UserExport.php +++ b/lib/Command/UserExport.php @@ -63,6 +63,9 @@ class UserExport extends Command { $this->groupManager = $groupManager; } + /** + * @return void + */ protected function configure() { $this ->setName('deck:export') diff --git a/lib/Command/fixtures/setting-schema.json b/lib/Command/fixtures/setting-schema.json index b21501c2b..f63cb3529 100644 --- a/lib/Command/fixtures/setting-schema.json +++ b/lib/Command/fixtures/setting-schema.json @@ -12,6 +12,9 @@ "type": "string", "required": true, "pattern": "^[0-9a-fA-F]{6}$" + }, + "translations": { + "type": "object" } } } \ No newline at end of file diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php new file mode 100644 index 000000000..a6d9c904c --- /dev/null +++ b/tests/unit/Command/BoardImportTest.php @@ -0,0 +1,77 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command; + +use OCA\Deck\Command\Helper\TrelloHelper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class BoardImportTest extends \Test\TestCase { + /** @var TrelloHelper */ + private $trelloHelper; + /** @var BoardImport */ + private $boardImport; + + public function setUp(): void { + parent::setUp(); + $this->trelloHelper = $this->createMock(TrelloHelper::class); + $this->boardImport = new BoardImport( + $this->trelloHelper + ); + $questionHelper = new QuestionHelper(); + $this->boardImport->setHelperSet( + new HelperSet([ + $questionHelper + ]) + ); + } + + public function testExecuteWithSuccess() { + $input = $this->createMock(InputInterface::class); + + $input->method('getOption') + ->withConsecutive( + [$this->equalTo('system')], + [$this->equalTo('setting')], + [$this->equalTo('data')] + ) + ->will($this->returnValueMap([ + ['system', 'trello'], + ['setting', __DIR__ . '/fixtures/setting-trello.json'], + ['data', __DIR__ . '/fixtures/data-trello.json'] + ])); + $output = $this->createMock(OutputInterface::class); + + $output + ->expects($this->once()) + ->method('writeLn') + ->with('Done!'); + + $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); + $actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]); + $this->assertEquals(0, $actual); + } +} diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php new file mode 100644 index 000000000..1e4dd6b8d --- /dev/null +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -0,0 +1,134 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command; + +use OCA\Deck\Command\Helper\TrelloHelper; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\LabelService; +use OCP\IDBConnection; +use OCP\IUserManager; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TrelloHelperTest extends \Test\TestCase { + /** @var BoardService */ + private $boardService; + /** @var LabelService */ + private $labelService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var IDBConnection */ + private $connection; + /** @var IUserManager */ + private $userManager; + /** @var TrelloHelper */ + private $trelloHelper; + public function setUp(): void { + parent::setUp(); + $this->boardService = $this->createMock(BoardService::class); + $this->labelService = $this->createMock(LabelService::class); + $this->stackMapper = $this->createMock(StackMapper::class); + $this->cardMapper = $this->createMock(CardMapper::class); + $this->assignmentMapper = $this->createMock(AssignmentMapper::class); + $this->aclMapper = $this->createMock(AclMapper::class); + $this->connection = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->trelloHelper = new TrelloHelper( + $this->boardService, + $this->labelService, + $this->stackMapper, + $this->cardMapper, + $this->assignmentMapper, + $this->aclMapper, + $this->connection, + $this->userManager + ); + $questionHelper = new QuestionHelper(); + $command = new BoardImport($this->trelloHelper); + $command->setHelperSet( + new HelperSet([ + $questionHelper + ]) + ); + $this->trelloHelper->setCommand($command); + } + + public function testImportWithSuccess() { + $input = $this->createMock(InputInterface::class); + + $input->method('getOption') + ->withConsecutive( + [$this->equalTo('data')], + [$this->equalTo('setting')] + ) + ->will($this->returnValueMap([ + ['data', __DIR__ . '/../fixtures/data-trello.json'], + ['setting', __DIR__ . '/../fixtures/setting-trello.json'] + ])); + $output = $this->createMock(OutputInterface::class); + + $user = $this->createMock(\OCP\IUser::class); + $user + ->method('getUID') + ->willReturn('admin'); + $this->userManager + ->method('get') + ->willReturn($user); + $this->userManager + ->method('get') + ->willReturn($user); + $board = $this->createMock(\OCA\Deck\Db\Board::class); + $this->boardService + ->expects($this->once()) + ->method('create') + ->willReturn($board); + $label = $this->createMock(\OCA\Deck\Db\Label::class); + $this->labelService + ->expects($this->once()) + ->method('create') + ->willReturn($label); + $stack = $this->createMock(\OCA\Deck\Db\Stack::class); + $this->stackMapper + ->expects($this->once()) + ->method('insert') + ->willReturn($stack); + $card = $this->createMock(\OCA\Deck\Db\Card::class); + $this->cardMapper + ->expects($this->once()) + ->method('insert') + ->willReturn($card); + + $this->trelloHelper->validate($input, $output); + $actual = $this->trelloHelper->import($input, $output); + $this->assertNull($actual); + } +} diff --git a/tests/unit/Command/fixtures/data-trello.json b/tests/unit/Command/fixtures/data-trello.json new file mode 100644 index 000000000..5d27a8c54 --- /dev/null +++ b/tests/unit/Command/fixtures/data-trello.json @@ -0,0 +1,582 @@ +{ + "id": "fakeboardidhash", + "name": "Test Board Name", + "desc": "", + "descData": null, + "closed": false, + "dateClosed": null, + "idOrganization": null, + "shortLink": "qwerty", + "powerUps": [], + "dateLastActivity": "2021-07-10T17:01:58.633Z", + "idTags": [], + "datePluginDisable": null, + "creationMethod": null, + "idBoardSource": null, + "idMemberCreator": "fakeidmemberhash", + "idEnterprise": null, + "pinned": false, + "starred": false, + "url": "https://trello.com/b/qwerty/fakeboardurl", + "prefs": { + "permissionLevel": "private", + "hideVotes": false, + "voting": "disabled", + "comments": "members", + "invitations": "members", + "selfJoin": false, + "cardCovers": true, + "isTemplate": false, + "cardAging": "regular", + "calendarFeedEnabled": false, + "background": "blue", + "backgroundImage": null, + "backgroundImageScaled": null, + "backgroundTile": false, + "backgroundBrightness": "dark", + "backgroundColor": "#0079BF", + "backgroundBottomColor": "#0079BF", + "backgroundTopColor": "#0079BF", + "canBePublic": true, + "canBeEnterprise": true, + "canBeOrg": true, + "canBePrivate": true, + "canInvite": true + }, + "shortUrl": "https://trello.com/b/qwerty", + "premiumFeatures": [], + "enterpriseOwned": false, + "ixUpdate": "67", + "limits": { + "attachments": { + "perBoard": { + "status": "ok", + "disableAt": 36000, + "warnAt": 32400 + }, + "perCard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "boards": { + "totalMembersPerBoard": { + "status": "ok", + "disableAt": 1600, + "warnAt": 1440 + } + }, + "cards": { + "openPerBoard": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "openPerList": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "totalPerBoard": { + "status": "ok", + "disableAt": 2000000, + "warnAt": 1800000 + }, + "totalPerList": { + "status": "ok", + "disableAt": 1000000, + "warnAt": 900000 + } + }, + "checklists": { + "perBoard": { + "status": "ok", + "disableAt": 2000000, + "warnAt": 1800000 + }, + "perCard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + } + }, + "checkItems": { + "perChecklist": { + "status": "ok", + "disableAt": 200, + "warnAt": 180 + } + }, + "customFields": { + "perBoard": { + "status": "ok", + "disableAt": 50, + "warnAt": 45 + } + }, + "customFieldOptions": { + "perField": { + "status": "ok", + "disableAt": 50, + "warnAt": 45 + } + }, + "labels": { + "perBoard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "lists": { + "openPerBoard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + }, + "totalPerBoard": { + "status": "ok", + "disableAt": 3000, + "warnAt": 2700 + } + }, + "stickers": { + "perCard": { + "status": "ok", + "disableAt": 70, + "warnAt": 63 + } + }, + "reactions": { + "perAction": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + }, + "uniquePerAction": { + "status": "ok", + "disableAt": 17, + "warnAt": 16 + } + } + }, + "subscribed": false, + "templateGallery": null, + "dateLastView": "2021-07-10T17:01:58.665Z", + "labelNames": { + "green": "", + "yellow": "", + "orange": "", + "red": "", + "purple": "", + "blue": "", + "sky": "", + "lime": "", + "pink": "", + "black": "" + }, + "actions": [ + { + "id": "60e9d2869efe2e1141be2798", + "idMemberCreator": "fakeidmemberhash", + "data": { + "idMember": "fakeidmemberhash", + "deactivated": false, + "card": { + "id": "hashcard7", + "name": "Name Card 7", + "idShort": 7, + "shortLink": "fakeshortlinkcard7" + }, + "board": { + "id": "fakeboardidhash", + "name": "Test Board Name", + "shortLink": "qwerty" + }, + "member": { + "id": "fakeidmemberhash", + "name": "John Doe" + } + }, + "type": "removeMemberFromCard", + "date": "2021-07-10T17:01:58.636Z", + "appCreator": null, + "limits": {}, + "member": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + }, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "60e9d1832ff82d10c0cea6ba", + "idMemberCreator": "fakeidmemberhash", + "data": { + "idMember": "fakeidmemberhash", + "card": { + "id": "hashcard7", + "name": "Name Card 7", + "idShort": 7, + "shortLink": "fakeshortlinkcard7" + }, + "board": { + "id": "fakeboardidhash", + "name": "Test Board Name", + "shortLink": "qwerty" + }, + "member": { + "id": "fakeidmemberhash", + "name": "John Doe" + } + }, + "type": "addMemberToCard", + "date": "2021-07-10T16:57:39.999Z", + "appCreator": null, + "limits": {}, + "member": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + }, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfc4bf36aa0270d6bfd43", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + }, + "list": { + "name": "TODO", + "id": "hashlisttodo" + }, + "card": { + "shortLink": "fakeshortlinkcard7", + "idShort": 7, + "name": "Name Card 7", + "id": "hashcard7" + } + }, + "type": "createCard", + "date": "2017-09-15T16:14:03.187Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfb8e4a6f8ca35be9b82a", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + }, + "list": { + "name": "TODO", + "id": "hashlisttodo" + } + }, + "type": "createList", + "date": "2017-09-15T16:10:54.714Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfb88973b76e586edec5e", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + } + }, + "type": "createBoard", + "date": "2017-09-15T16:10:48.069Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + } + ], + "cards": [ + { + "id": "hashcard7", + "address": null, + "checkItemStates": null, + "closed": false, + "coordinates": null, + "creationMethod": null, + "dateLastActivity": "2021-07-10T17:01:58.633Z", + "desc": "", + "descData": null, + "dueReminder": null, + "idBoard": "fakeboardidhash", + "idLabels": [], + "idList": "hashlisttodo", + "idMembersVoted": [], + "idShort": 7, + "idAttachmentCover": null, + "locationName": null, + "manualCoverAttachment": false, + "name": "Name Card 7", + "pos": 65535, + "shortLink": "fakeshortlinkcard7", + "isTemplate": false, + "cardRole": null, + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 0, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 0, + "attachments": 0, + "description": false, + "due": null, + "dueComplete": false, + "start": null + }, + "dueComplete": false, + "due": null, + "email": "johndoe+card7@boards.trello.com", + "idChecklists": [], + "idMembers": [], + "labels": [], + "limits": { + "attachments": { + "perCard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "checklists": { + "perCard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + } + }, + "stickers": { + "perCard": { + "status": "ok", + "disableAt": 70, + "warnAt": 63 + } + } + }, + "shortUrl": "https://trello.com/c/fakeshortlinkcard7", + "start": null, + "subscribed": false, + "url": "https://trello.com/c/fakeshortlinkcard7/7-name-card-7", + "cover": { + "idAttachment": null, + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "attachments": [], + "pluginData": [], + "customFieldItems": [] + } + ], + "labels": [ + { + "id": "59bbfb881314a339999eb855", + "idBoard": "fakeboardidhash", + "name": "", + "color": "yellow" + } + ], + "lists": [ + { + "id": "hashlisttodo", + "name": "TODO", + "closed": false, + "pos": 65535, + "softLimit": null, + "creationMethod": null, + "idBoard": "fakeboardidhash", + "limits": { + "cards": { + "openPerList": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "totalPerList": { + "status": "ok", + "disableAt": 1000000, + "warnAt": 900000 + } + } + }, + "subscribed": false + } + ], + "members": [ + { + "id": "fakeidmemberhash", + "bio": "", + "bioData": { + "emoji": {} + }, + "confirmed": true, + "memberType": "normal", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idEnterprise": null, + "idEnterprisesDeactivated": [], + "idMemberReferrer": null, + "idPremOrgsAdmin": [], + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true, + "products": [], + "url": "https://trello.com/johndoe", + "status": "disconnected" + } + ], + "checklists": [], + "customFields": [], + "memberships": [ + { + "id": "59bbfb88973b76e586edec5d", + "idMember": "fakeidmemberhash", + "memberType": "admin", + "unconfirmed": false, + "deactivated": false + } + ], + "pluginData": [] +} \ No newline at end of file diff --git a/tests/unit/Command/fixtures/setting-trello.json b/tests/unit/Command/fixtures/setting-trello.json new file mode 100644 index 000000000..544118212 --- /dev/null +++ b/tests/unit/Command/fixtures/setting-trello.json @@ -0,0 +1,7 @@ +{ + "owner": "admin", + "color": "0800fd", + "uidRelation": { + "johndoe": "admin" + } +} \ No newline at end of file From e28a47e9e0cc928e04087a548a914976401f2a96 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sat, 10 Jul 2021 21:39:46 -0300 Subject: [PATCH 03/21] Update UserExport.php Signed-off-by: Vitor Mattos --- lib/Command/UserExport.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Command/UserExport.php b/lib/Command/UserExport.php index 0b595c773..1d73e8dcf 100644 --- a/lib/Command/UserExport.php +++ b/lib/Command/UserExport.php @@ -63,9 +63,6 @@ class UserExport extends Command { $this->groupManager = $groupManager; } - /** - * @return void - */ protected function configure() { $this ->setName('deck:export') From eb8bf3f22be5dfa1f3dbb0250ac069353d023498 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 11 Jul 2021 01:47:45 -0300 Subject: [PATCH 04/21] Translations and tests Signed-off-by: Vitor Mattos --- lib/Command/Helper/TrelloHelper.php | 27 ++++++++++--------- lib/Command/fixtures/setting-schema.json | 3 --- .../unit/Command/Helper/TrelloHelperTest.php | 7 ++++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/Command/Helper/TrelloHelper.php b/lib/Command/Helper/TrelloHelper.php index 50e1be346..102c8e282 100644 --- a/lib/Command/Helper/TrelloHelper.php +++ b/lib/Command/Helper/TrelloHelper.php @@ -36,6 +36,7 @@ use OCA\Deck\Db\StackMapper; use OCA\Deck\Service\BoardService; use OCA\Deck\Service\LabelService; use OCP\IDBConnection; +use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; use Symfony\Component\Console\Input\InputInterface; @@ -57,12 +58,12 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { private $connection; /** @var IUserManager */ private $userManager; - /** @var TrelloActions */ - private $trelloActions; /** @var Board */ private $board; /** @var LabelService */ private $labelService; + /** @var IL10N */ + private $l10n; /** * Data object created from JSON of origin system * @@ -83,7 +84,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { private $labels = []; /** @var Card[] */ private $cards = []; - /** @var IUser */ + /** @var IUser[] */ private $members = []; public function __construct( @@ -94,7 +95,8 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { AssignmentMapper $assignmentMapper, AclMapper $aclMapper, IDBConnection $connection, - IUserManager $userManager + IUserManager $userManager, + IL10N $l10n ) { $this->boardService = $boardService; $this->labelService = $labelService; @@ -104,6 +106,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { $this->aclMapper = $aclMapper; $this->connection = $connection; $this->userManager = $userManager; + $this->l10n = $l10n; } public function validate(InputInterface $input, OutputInterface $output): void { @@ -127,7 +130,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { $this->importCards(); } - private function assignUsersToBoard() { + private function assignUsersToBoard(): void { foreach ($this->members as $member) { $acl = new Acl(); $acl->setBoardId($this->board->getId()); @@ -260,17 +263,15 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } } + /** + * @return void + */ private function appendAttachmentsToDescription($trelloCard) { if (empty($trelloCard->attachments)) { return; } - $translations = $this->getSetting('translations'); - $attachmentsLabel = empty($translations->{'Attachments'}) ? 'Attachments' : $translations->{'Attachments'}; - $URLLabel = empty($translations->{'URL'}) ? 'URL' : $translations->{'URL'}; - $nameLabel = empty($translations->{'Name'}) ? 'Name' : $translations->{'Name'}; - $dateLabel = empty($translations->{'Date'}) ? 'Date' : $translations->{'Date'}; - $trelloCard->desc .= "\n\n## {$attachmentsLabel}\n"; - $trelloCard->desc .= "| $URLLabel | $nameLabel | $dateLabel |\n"; + $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; + $trelloCard->desc .= "| {$this->l10n->t('URL')} | {$this->l10n->t('Name')} | {$this->l10n->t('date')} |\n"; $trelloCard->desc .= "|---|---|---|\n"; foreach ($trelloCard->attachments as $attachment) { $name = $attachment->name === $attachment->url ? null : $attachment->name; @@ -278,7 +279,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } } - private function assignToMember(Card $card, $trelloCard) { + private function assignToMember(Card $card, $trelloCard): void { foreach ($trelloCard->idMembers as $idMember) { $assignment = new Assignment(); $assignment->setCardId($card->getId()); diff --git a/lib/Command/fixtures/setting-schema.json b/lib/Command/fixtures/setting-schema.json index f63cb3529..b21501c2b 100644 --- a/lib/Command/fixtures/setting-schema.json +++ b/lib/Command/fixtures/setting-schema.json @@ -12,9 +12,6 @@ "type": "string", "required": true, "pattern": "^[0-9a-fA-F]{6}$" - }, - "translations": { - "type": "object" } } } \ No newline at end of file diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php index 1e4dd6b8d..e93889efd 100644 --- a/tests/unit/Command/Helper/TrelloHelperTest.php +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -31,6 +31,7 @@ use OCA\Deck\Db\StackMapper; use OCA\Deck\Service\BoardService; use OCA\Deck\Service\LabelService; use OCP\IDBConnection; +use OCP\IL10N; use OCP\IUserManager; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; @@ -52,6 +53,8 @@ class TrelloHelperTest extends \Test\TestCase { private $userManager; /** @var TrelloHelper */ private $trelloHelper; + /** @var IL10N */ + private $l10n; public function setUp(): void { parent::setUp(); $this->boardService = $this->createMock(BoardService::class); @@ -62,6 +65,7 @@ class TrelloHelperTest extends \Test\TestCase { $this->aclMapper = $this->createMock(AclMapper::class); $this->connection = $this->createMock(IDBConnection::class); $this->userManager = $this->createMock(IUserManager::class); + $this->l10n = $this->createMock(IL10N::class); $this->trelloHelper = new TrelloHelper( $this->boardService, $this->labelService, @@ -70,7 +74,8 @@ class TrelloHelperTest extends \Test\TestCase { $this->assignmentMapper, $this->aclMapper, $this->connection, - $this->userManager + $this->userManager, + $this->l10n ); $questionHelper = new QuestionHelper(); $command = new BoardImport($this->trelloHelper); From fd92fc3c4dd04541735d013f466035e0fd595ab6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 12 Jul 2021 10:42:36 -0300 Subject: [PATCH 05/21] Refactor and improvements on command Check available helpers Default permission: view only Moved validate setting from helper to command Turn more easy create a importer Docblock and improvements on interface lcfirst on system property Helper moved to ImporHelper folder Moved fixtures to ImportHelper Rename settings to config Big refactor to move import methods to service Signed-off-by: Vitor Mattos --- lib/Command/BoardImport.php | 78 +++++++-- lib/Command/Helper/ImportAbstract.php | 86 ---------- lib/Command/ImportHelper/AImport.php | 75 +++++++++ .../ImportInterface.php | 34 +++- lib/Command/ImportHelper/TrelloHelper.php | 76 +++++++++ .../fixtures/config-trello-schema.json | 24 +++ lib/Command/fixtures/setting-schema.json | 17 -- lib/Service/AImportService.php | 49 ++++++ .../TrelloImportService.php} | 151 +++++++----------- tests/unit/Command/BoardImportTest.php | 6 +- .../unit/Command/Helper/TrelloHelperTest.php | 90 ++--------- ...setting-trello.json => config-trello.json} | 0 .../unit/Service/TrelloImportServiceTest.php | 104 ++++++++++++ 13 files changed, 494 insertions(+), 296 deletions(-) delete mode 100644 lib/Command/Helper/ImportAbstract.php create mode 100644 lib/Command/ImportHelper/AImport.php rename lib/Command/{Helper => ImportHelper}/ImportInterface.php (69%) create mode 100644 lib/Command/ImportHelper/TrelloHelper.php create mode 100644 lib/Command/ImportHelper/fixtures/config-trello-schema.json delete mode 100644 lib/Command/fixtures/setting-schema.json create mode 100644 lib/Service/AImportService.php rename lib/{Command/Helper/TrelloHelper.php => Service/TrelloImportService.php} (73%) rename tests/unit/Command/fixtures/{setting-trello.json => config-trello.json} (100%) create mode 100644 tests/unit/Service/TrelloImportServiceTest.php diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 0870c52a1..ac1ebaab2 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -23,26 +23,23 @@ namespace OCA\Deck\Command; -use OCA\Deck\Command\Helper\ImportInterface; -use OCA\Deck\Command\Helper\TrelloHelper; +use JsonSchema\Constraints\Constraint; +use JsonSchema\Validator; +use OCA\Deck\Command\ImportHelper\AImport; +use OCA\Deck\Command\ImportHelper\TrelloHelper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; class BoardImport extends Command { /** @var string */ private $system; - private $allowedSystems = ['trello']; + private $allowedSystems; /** @var TrelloHelper */ private $trelloHelper; - /** - * Data object created from settings JSON - * - * @var \StdClass - */ - public $settings; public function __construct( TrelloHelper $trelloHelper @@ -55,6 +52,11 @@ class BoardImport extends Command { * @return void */ protected function configure() { + $allowedSystems = glob(__DIR__ . '/ImportHelper/*Helper.php'); + $this->allowedSystems = array_map(function ($name) { + preg_match('/\/(?\w+)Helper\.php$/', $name, $matches); + return lcfirst($matches['system']); + }, $allowedSystems); $this ->setName('deck:import') ->setDescription('Import data') @@ -62,11 +64,11 @@ class BoardImport extends Command { 'system', null, InputOption::VALUE_REQUIRED, - 'Source system for import. Available options: trello.', + 'Source system for import. Available options: ' . implode(', ', $this->allowedSystems) . '.', 'trello' ) ->addOption( - 'setting', + 'config', null, InputOption::VALUE_REQUIRED, 'Configuration json file.', @@ -89,18 +91,64 @@ class BoardImport extends Command { */ protected function interact(InputInterface $input, OutputInterface $output) { $this->validateSystem($input, $output); - $this->getSystem() + $this->validateConfig($input, $output); + $this->getSystemHelper() ->validate($input, $output); } + protected function validateConfig(InputInterface $input, OutputInterface $output): void { + $configFile = $input->getOption('config'); + if (!is_file($configFile)) { + $helper = $this->getHelper('question'); + $question = new Question( + 'Please inform a valid config json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'config file not found' + ); + } + return $answer; + }); + $configFile = $helper->ask($input, $output, $question); + $input->setOption('config', $configFile); + } + + $config = json_decode(file_get_contents($configFile)); + $schemaPath = __DIR__ . '/ImportHelper/fixtures/config-' . $this->getSystem() . '-schema.json'; + $validator = new Validator(); + $validator->validate( + $config, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + $output->writeln('Invalid config file'); + $output->writeln(array_map(function ($v) { + return $v['message']; + }, $validator->getErrors())); + $output->writeln('Valid schema:'); + $output->writeln(print_r(file_get_contents($schemaPath), true)); + $input->setOption('config', null); + $this->validateConfig($input, $output); + } + $this->getSystemHelper()->setConfigInstance($config); + } + private function setSystem(string $system): void { $this->system = $system; } + public function getSystem() { + return $this->system; + } + /** - * @return ImportInterface + * @return AImport */ - private function getSystem() { + private function getSystemHelper() { $helper = $this->{$this->system . 'Helper'}; $helper->setCommand($this); return $helper; @@ -134,7 +182,7 @@ class BoardImport extends Command { * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->getSystem() + $this->getSystemHelper() ->import($input, $output); $output->writeln('Done!'); return 0; diff --git a/lib/Command/Helper/ImportAbstract.php b/lib/Command/Helper/ImportAbstract.php deleted file mode 100644 index de271f2c8..000000000 --- a/lib/Command/Helper/ImportAbstract.php +++ /dev/null @@ -1,86 +0,0 @@ -command = $command; - } - - /** - * @return Command - */ - public function getCommand() { - return $this->command; - } - - /** - * Get a setting - * - * @param string $setting Setting name - * @return mixed - */ - public function getSetting($setting) { - return $this->settings->$setting; - } - - /** - * Define a setting - * - * @param string $settingName - * @param mixed $value - * @return void - */ - public function setSetting($settingName, $value) { - $this->settings->$settingName = $value; - } - - protected function validateSettings(InputInterface $input, OutputInterface $output): void { - $settingFile = $input->getOption('setting'); - if (!is_file($settingFile)) { - $helper = $this->getCommand()->getHelper('question'); - $question = new Question( - 'Please inform a valid setting json file: ', - 'config.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Setting file not found' - ); - } - return $answer; - }); - $settingFile = $helper->ask($input, $output, $question); - $input->setOption('setting', $settingFile); - } - - $this->settings = json_decode(file_get_contents($settingFile)); - $validator = new Validator(); - $validator->validate( - $this->settings, - (object)['$ref' => 'file://' . realpath(__DIR__ . '/../fixtures/setting-schema.json')] - ); - if (!$validator->isValid()) { - $output->writeln('Invalid setting file'); - $output->writeln(array_map(function ($v) { - return $v['message']; - }, $validator->getErrors())); - $output->writeln('Valid schema:'); - $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); - $input->setOption('setting', null); - $this->validateSettings($input, $output); - } - } -} diff --git a/lib/Command/ImportHelper/AImport.php b/lib/Command/ImportHelper/AImport.php new file mode 100644 index 000000000..c7ba56cce --- /dev/null +++ b/lib/Command/ImportHelper/AImport.php @@ -0,0 +1,75 @@ +trelloImportService = $trelloImportService; + } + + abstract public function validate(InputInterface $input, OutputInterface $output): void; + + abstract public function import(InputInterface $input, OutputInterface $output): void; + + /** + * Define Command instance + * + * @param Command $command + * @return void + */ + public function setCommand(Command $command): void { + $this->command = $command; + } + + /** + * @return BoardImport + */ + public function getCommand() { + return $this->command; + } + + public function setConfigInstance(\stdClass $config) { + $this->trelloImportService->setConfigInstance($config); + } + + /** + * Define a config + * + * @param string $configName + * @param mixed $value + * @return void + */ + public function setConfig(string $configName, $value): void { + $this->trelloImportService->setConfig($configName, $value); + } + + /** + * Get a config + * + * @param string $configName config name + * @return mixed + */ + public function getConfig(string $configName = null) { + return $this->trelloImportService->getConfig($configName); + } +} diff --git a/lib/Command/Helper/ImportInterface.php b/lib/Command/ImportHelper/ImportInterface.php similarity index 69% rename from lib/Command/Helper/ImportInterface.php rename to lib/Command/ImportHelper/ImportInterface.php index 08f0e6a68..f5d68794c 100644 --- a/lib/Command/Helper/ImportInterface.php +++ b/lib/Command/ImportHelper/ImportInterface.php @@ -21,8 +21,10 @@ * */ -namespace OCA\Deck\Command\Helper; +namespace OCA\Deck\Command\ImportHelper; +use OCA\Deck\Command\BoardImport; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -44,4 +46,34 @@ interface ImportInterface { * @return void */ public function import(InputInterface $input, OutputInterface $output): void; + + /** + * Define Command instance + * + * @param Command $command + * @return void + */ + public function setCommand(Command $command): void; + + /** + * @return BoardImport + */ + public function getCommand(); + + /** + * Define a config + * + * @param string $configName + * @param mixed $value + * @return void + */ + public function setConfig(string $configName, $value): void; + + /** + * Get a config + * + * @param string $configName config name + * @return mixed + */ + public function getConfig($configName); } diff --git a/lib/Command/ImportHelper/TrelloHelper.php b/lib/Command/ImportHelper/TrelloHelper.php new file mode 100644 index 000000000..448f8e629 --- /dev/null +++ b/lib/Command/ImportHelper/TrelloHelper.php @@ -0,0 +1,76 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command\ImportHelper; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class TrelloHelper extends AImport { + public function validate(InputInterface $input, OutputInterface $output): void { + $this->validateData($input, $output); + $this->trelloImportService->validateOwner(); + $this->trelloImportService->validateUsers(); + } + + public function import(InputInterface $input, OutputInterface $output): void { + $this->trelloImportService->setUserId(); + $output->writeln('Importing board...'); + $this->trelloImportService->importBoard(); + $output->writeln('Assign users to board...'); + $this->trelloImportService->assignUsersToBoard(); + $output->writeln('Importing labels...'); + $this->trelloImportService->importLabels(); + $output->writeln('Importing stacks...'); + $this->trelloImportService->importStacks(); + $output->writeln('Importing cards...'); + $this->trelloImportService->importCards(); + } + + private function validateData(InputInterface $input, OutputInterface $output): void { + $filename = $input->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($input, $output, $question); + $input->setOption('data', $data); + } + $this->trelloImportService->setData(json_decode(file_get_contents($filename))); + if (!$this->trelloImportService->getData()) { + $output->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($input, $output); + } + } +} diff --git a/lib/Command/ImportHelper/fixtures/config-trello-schema.json b/lib/Command/ImportHelper/fixtures/config-trello-schema.json new file mode 100644 index 000000000..7635727c1 --- /dev/null +++ b/lib/Command/ImportHelper/fixtures/config-trello-schema.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "properties": { + "uidRelation": { + "type": "object", + "comment": "Relationship between Trello and Nextcloud usernames", + "example": { + "johndoe": "admin" + } + }, + "owner": { + "type": "string", + "required": true, + "comment": "Nextcloud owner username" + }, + "color": { + "type": "string", + "required": true, + "pattern": "^[0-9a-fA-F]{6}$", + "comment": "Default color for the board. If you don't inform, the default color will be used.", + "default": "0800fd" + } + } +} \ No newline at end of file diff --git a/lib/Command/fixtures/setting-schema.json b/lib/Command/fixtures/setting-schema.json deleted file mode 100644 index b21501c2b..000000000 --- a/lib/Command/fixtures/setting-schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "object", - "properties": { - "uidRelation": { - "type": "object" - }, - "owner": { - "type": "string", - "required": true - }, - "color": { - "type": "string", - "required": true, - "pattern": "^[0-9a-fA-F]{6}$" - } - } -} \ No newline at end of file diff --git a/lib/Service/AImportService.php b/lib/Service/AImportService.php new file mode 100644 index 000000000..3ca20226b --- /dev/null +++ b/lib/Service/AImportService.php @@ -0,0 +1,49 @@ +config = $config; + } + + /** + * Define a config + * + * @param string $configName + * @param mixed $value + * @return void + */ + public function setConfig(string $configName, $value): void { + if (!$this->config) { + $this->setConfigInstance(new \stdClass); + } + $this->config->$configName = $value; + } + + /** + * Get a config + * + * @param string $configName config name + * @return mixed + */ + public function getConfig(string $configName = null) { + if (!is_object($this->config)) { + return; + } + if (!$configName) { + return $this->config; + } + if (!property_exists($this->config, $configName)) { + return; + } + return $this->config->$configName; + } +} diff --git a/lib/Command/Helper/TrelloHelper.php b/lib/Service/TrelloImportService.php similarity index 73% rename from lib/Command/Helper/TrelloHelper.php rename to lib/Service/TrelloImportService.php index 102c8e282..c09251579 100644 --- a/lib/Command/Helper/TrelloHelper.php +++ b/lib/Service/TrelloImportService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Command\Helper; +namespace OCA\Deck\Service; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; @@ -33,19 +33,16 @@ use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; -use OCA\Deck\Service\BoardService; -use OCA\Deck\Service\LabelService; use OCP\IDBConnection; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -class TrelloHelper extends ImportAbstract implements ImportInterface { +class TrelloImportService extends AImportService { /** @var BoardService */ private $boardService; + /** @var LabelService */ + private $labelService; /** @var StackMapper */ private $stackMapper; /** @var CardMapper */ @@ -58,18 +55,10 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { private $connection; /** @var IUserManager */ private $userManager; - /** @var Board */ - private $board; - /** @var LabelService */ - private $labelService; /** @var IL10N */ private $l10n; - /** - * Data object created from JSON of origin system - * - * @var \StdClass - */ - private $data; + /** @var Board */ + private $board; /** * Array of stacks * @@ -86,6 +75,12 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { private $cards = []; /** @var IUser[] */ private $members = []; + /** + * Data object created from JSON of origin system + * + * @var \StdClass + */ + private $data; public function __construct( BoardService $boardService, @@ -109,82 +104,30 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { $this->l10n = $l10n; } - public function validate(InputInterface $input, OutputInterface $output): void { - $this->validateData($input, $output); - $this->validateSettings($input, $output); - $this->validateUsers(); - $this->validateOwner(); + public function setData(\stdClass $data) { + $this->data = $data; } - public function import(InputInterface $input, OutputInterface $output): void { - $this->setUserId(); - $output->writeln('Importing board...'); - $this->importBoard(); - $output->writeln('Assign users to board...'); - $this->assignUsersToBoard(); - $output->writeln('Importing labels...'); - $this->importLabels(); - $output->writeln('Importing stacks...'); - $this->importStacks(); - $output->writeln('Importing cards...'); - $this->importCards(); + public function getData() { + return $this->data; } - private function assignUsersToBoard(): void { - foreach ($this->members as $member) { - $acl = new Acl(); - $acl->setBoardId($this->board->getId()); - $acl->setType(Acl::PERMISSION_TYPE_USER); - $acl->setParticipant($member->getUid()); - $acl->setPermissionEdit(true); - $acl->setPermissionShare($member->getUID() === $this->getSetting('owner')->getUID()); - $acl->setPermissionManage($member->getUID() === $this->getSetting('owner')->getUID()); - $this->aclMapper->insert($acl); - } - } - - private function validateData(InputInterface $input, OutputInterface $output): void { - $filename = $input->getOption('data'); - if (!is_file($filename)) { - $helper = $this->getCommand()->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($input, $output, $question); - $input->setOption('data', $data); - } - $this->data = json_decode(file_get_contents($filename)); - if (!$this->data) { - $output->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($input, $output); - } - } - - private function validateOwner(): void { - $owner = $this->userManager->get($this->getSetting('owner')); + public function validateOwner(): void { + $owner = $this->userManager->get($this->getConfig('owner')); if (!$owner) { - throw new \LogicException('Owner "' . $this->getSetting('owner') . '" not found on Nextcloud. Check setting json.'); + throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); } - $this->setSetting('owner', $owner); + $this->setConfig('owner', $owner); } /** * @return void */ - private function validateUsers() { - if (empty($this->getSetting('uidRelation'))) { + public function validateUsers() { + if (empty($this->getConfig('uidRelation'))) { return; } - foreach ($this->getSetting('uidRelation') as $trelloUid => $nextcloudUid) { + foreach ($this->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { $user = array_filter($this->data->members, function ($u) use ($trelloUid) { return $u->username === $trelloUid; }); @@ -194,12 +137,28 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { if (!is_string($nextcloudUid)) { throw new \LogicException('User on setting uidRelation must be a string'); } - $this->getSetting('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); - if (!$this->getSetting('uidRelation')->$trelloUid) { + $this->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->getConfig('uidRelation')->$trelloUid) { throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); } $user = current($user); - $this->members[$user->id] = $this->getSetting('uidRelation')->$trelloUid; + $this->members[$user->id] = $this->getConfig('uidRelation')->$trelloUid; + } + } + + public function assignUsersToBoard(): void { + foreach ($this->members as $member) { + if ($member->getUID() === $this->getConfig('owner')->getUID()) { + continue; + } + $acl = new Acl(); + $acl->setBoardId($this->board->getId()); + $acl->setType(Acl::PERMISSION_TYPE_USER); + $acl->setParticipant($member->getUID()); + $acl->setPermissionEdit(false); + $acl->setPermissionShare(false); + $acl->setPermissionManage(false); + $this->aclMapper->insert($acl); } } @@ -222,7 +181,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { return $checklist_string; } - private function importCards(): void { + public function importCards(): void { $checklists = []; foreach ($this->data->checklists as $checklist) { $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); @@ -247,7 +206,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { $card->setStackId($this->stacks[$trelloCard->idList]->getId()); $card->setType('plain'); $card->setOrder($trelloCard->idShort); - $card->setOwner($this->getSetting('owner')->getUID()); + $card->setOwner($this->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) @@ -297,10 +256,10 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } ); foreach ($comments as $trelloComment) { - if (!empty($this->getSetting('uidRelation')->{$trelloComment->memberCreator->username})) { - $actor = $this->getSetting('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + if (!empty($this->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); } else { - $actor = $this->getSetting('owner')->getUID(); + $actor = $this->getConfig('owner')->getUID(); } $message = $this->replaceUsernames($trelloComment->data->text); $qb = $this->connection->getQueryBuilder(); @@ -329,7 +288,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } private function replaceUsernames($text) { - foreach ($this->getSetting('uidRelation') as $trello => $nextcloud) { + foreach ($this->getConfig('uidRelation') as $trello => $nextcloud) { $text = str_replace($trello, $nextcloud->getUID(), $text); } return $text; @@ -344,7 +303,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } } - private function importStacks(): void { + public function importStacks(): void { $this->stacks = []; foreach ($this->data->lists as $order => $list) { $stack = new Stack(); @@ -386,15 +345,15 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } } - private function importBoard(): void { + public function importBoard(): void { $this->board = $this->boardService->create( $this->data->name, - $this->getSetting('owner')->getUID(), - $this->getSetting('color') + $this->getConfig('owner')->getUID(), + $this->getConfig('color') ); } - private function importLabels(): void { + public function importLabels(): void { $this->labels = []; foreach ($this->data->labels as $label) { if (empty($label->name)) { @@ -411,7 +370,7 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { } } - private function setUserId(): void { + public function setUserId(): void { if (!property_exists($this->labelService, 'permissionService')) { return; } @@ -425,6 +384,6 @@ class TrelloHelper extends ImportAbstract implements ImportInterface { $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); $propertyUserId->setAccessible(true); - $propertyUserId->setValue($permissionService, $this->getSetting('owner')->getUID()); + $propertyUserId->setValue($permissionService, $this->getConfig('owner')->getUID()); } } diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index a6d9c904c..77eac621b 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -23,7 +23,7 @@ namespace OCA\Deck\Command; -use OCA\Deck\Command\Helper\TrelloHelper; +use OCA\Deck\Command\ImportHelper\TrelloHelper; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -55,12 +55,12 @@ class BoardImportTest extends \Test\TestCase { $input->method('getOption') ->withConsecutive( [$this->equalTo('system')], - [$this->equalTo('setting')], + [$this->equalTo('config')], [$this->equalTo('data')] ) ->will($this->returnValueMap([ ['system', 'trello'], - ['setting', __DIR__ . '/fixtures/setting-trello.json'], + ['config', __DIR__ . '/fixtures/config-trello.json'], ['data', __DIR__ . '/fixtures/data-trello.json'] ])); $output = $this->createMock(OutputInterface::class); diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php index e93889efd..006436ed0 100644 --- a/tests/unit/Command/Helper/TrelloHelperTest.php +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -23,59 +23,23 @@ namespace OCA\Deck\Command; -use OCA\Deck\Command\Helper\TrelloHelper; -use OCA\Deck\Db\AclMapper; -use OCA\Deck\Db\AssignmentMapper; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\StackMapper; -use OCA\Deck\Service\BoardService; -use OCA\Deck\Service\LabelService; -use OCP\IDBConnection; -use OCP\IL10N; -use OCP\IUserManager; +use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\TrelloImportService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class TrelloHelperTest extends \Test\TestCase { - /** @var BoardService */ - private $boardService; - /** @var LabelService */ - private $labelService; - /** @var StackMapper */ - private $stackMapper; - /** @var CardMapper */ - private $cardMapper; - /** @var IDBConnection */ - private $connection; - /** @var IUserManager */ - private $userManager; + /** @var TrelloImportService */ + private $trelloImportService; /** @var TrelloHelper */ private $trelloHelper; - /** @var IL10N */ - private $l10n; public function setUp(): void { parent::setUp(); - $this->boardService = $this->createMock(BoardService::class); - $this->labelService = $this->createMock(LabelService::class); - $this->stackMapper = $this->createMock(StackMapper::class); - $this->cardMapper = $this->createMock(CardMapper::class); - $this->assignmentMapper = $this->createMock(AssignmentMapper::class); - $this->aclMapper = $this->createMock(AclMapper::class); - $this->connection = $this->createMock(IDBConnection::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->l10n = $this->createMock(IL10N::class); + $this->trelloImportService = $this->createMock(TrelloImportService::class); $this->trelloHelper = new TrelloHelper( - $this->boardService, - $this->labelService, - $this->stackMapper, - $this->cardMapper, - $this->assignmentMapper, - $this->aclMapper, - $this->connection, - $this->userManager, - $this->l10n + $this->trelloImportService ); $questionHelper = new QuestionHelper(); $command = new BoardImport($this->trelloHelper); @@ -92,47 +56,17 @@ class TrelloHelperTest extends \Test\TestCase { $input->method('getOption') ->withConsecutive( - [$this->equalTo('data')], - [$this->equalTo('setting')] + [$this->equalTo('system')], + [$this->equalTo('config')] ) ->will($this->returnValueMap([ - ['data', __DIR__ . '/../fixtures/data-trello.json'], - ['setting', __DIR__ . '/../fixtures/setting-trello.json'] + ['system', 'trello'], + ['config', __DIR__ . '/../fixtures/config-trello.json'] ])); $output = $this->createMock(OutputInterface::class); - $user = $this->createMock(\OCP\IUser::class); - $user - ->method('getUID') - ->willReturn('admin'); - $this->userManager - ->method('get') - ->willReturn($user); - $this->userManager - ->method('get') - ->willReturn($user); - $board = $this->createMock(\OCA\Deck\Db\Board::class); - $this->boardService - ->expects($this->once()) - ->method('create') - ->willReturn($board); - $label = $this->createMock(\OCA\Deck\Db\Label::class); - $this->labelService - ->expects($this->once()) - ->method('create') - ->willReturn($label); - $stack = $this->createMock(\OCA\Deck\Db\Stack::class); - $this->stackMapper - ->expects($this->once()) - ->method('insert') - ->willReturn($stack); - $card = $this->createMock(\OCA\Deck\Db\Card::class); - $this->cardMapper - ->expects($this->once()) - ->method('insert') - ->willReturn($card); - - $this->trelloHelper->validate($input, $output); + $this->invokePrivate($this->trelloHelper->getCommand(), 'validateSystem', [$input, $output]); + $this->invokePrivate($this->trelloHelper->getCommand(), 'validateConfig', [$input, $output]); $actual = $this->trelloHelper->import($input, $output); $this->assertNull($actual); } diff --git a/tests/unit/Command/fixtures/setting-trello.json b/tests/unit/Command/fixtures/config-trello.json similarity index 100% rename from tests/unit/Command/fixtures/setting-trello.json rename to tests/unit/Command/fixtures/config-trello.json diff --git a/tests/unit/Service/TrelloImportServiceTest.php b/tests/unit/Service/TrelloImportServiceTest.php new file mode 100644 index 000000000..d9bc2ed50 --- /dev/null +++ b/tests/unit/Service/TrelloImportServiceTest.php @@ -0,0 +1,104 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Service; + +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\StackMapper; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IUserManager; + +class TrelloImportServiceTest extends \Test\TestCase { + /** @var TrelloImportService */ + private $trelloImportService; + /** @var BoardService */ + private $boardService; + /** @var LabelService */ + private $labelService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var AssignmentMapper */ + private $assignmentMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var IDBConnection */ + private $connection; + /** @var IUserManager */ + private $userManager; + /** @var IL10N */ + private $l10n; + public function setUp(): void { + parent::setUp(); + $this->boardService = $this->createMock(BoardService::class); + $this->labelService = $this->createMock(LabelService::class); + $this->stackMapper = $this->createMock(StackMapper::class); + $this->cardMapper = $this->createMock(CardMapper::class); + $this->assignmentMapper = $this->createMock(AssignmentMapper::class); + $this->aclMapper = $this->createMock(AclMapper::class); + $this->connection = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->trelloImportService = new TrelloImportService( + $this->boardService, + $this->labelService, + $this->stackMapper, + $this->cardMapper, + $this->assignmentMapper, + $this->aclMapper, + $this->connection, + $this->userManager, + $this->l10n + ); + } + + public function testValidateOwnerWithFaliure() { + $owner = $this->createMock(\OCP\IUser::class); + $owner + ->method('getUID') + ->willReturn('admin'); + $this->trelloImportService->setConfig('owner', $owner); + $this->userManager + ->method('get') + ->willReturn(null); + $this->expectErrorMessage('Owner "admin" not found on Nextcloud. Check setting json.'); + $this->trelloImportService->validateOwner(); + } + + public function testValidateOwnerWithSuccess() { + $owner = $this->createMock(\OCP\IUser::class); + $owner + ->method('getUID') + ->willReturn('admin'); + $this->trelloImportService->setConfig('owner', $owner); + $this->userManager + ->method('get') + ->willReturn($owner); + $actual = $this->trelloImportService->validateOwner(); + $this->assertNull($actual); + } +} From c5d10dafb85c63c22c384220e243a101b3cfc6d2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 14 Jul 2021 23:49:31 -0300 Subject: [PATCH 06/21] Import participants Big refactor to create route Import participants Signed-off-by: Vitor Mattos --- appinfo/routes.php | 2 + lib/Command/BoardImport.php | 120 +----- lib/Command/ImportHelper/AImport.php | 75 ---- lib/Command/ImportHelper/ImportInterface.php | 79 ---- lib/Command/ImportHelper/TrelloHelper.php | 76 ---- lib/Controller/BoardImportApiController.php | 50 +++ lib/Service/ABoardImportService.php | 53 +++ lib/Service/AImportService.php | 49 --- lib/Service/BoardImportCommandService.php | 180 +++++++++ lib/Service/BoardImportService.php | 363 ++++++++++++++++++ ...rvice.php => BoardImportTrelloService.php} | 246 ++++++------ .../fixtures/config-trello-schema.json | 0 .../fixtures => data}/config-trello.json | 0 .../fixtures => data}/data-trello.json | 0 tests/unit/Command/BoardImportTest.php | 23 +- .../unit/Command/Helper/TrelloHelperTest.php | 23 +- tests/unit/Service/BoardImportServiceTest.php | 46 +++ tests/unit/Service/BoardServiceTest.php | 4 + 18 files changed, 868 insertions(+), 521 deletions(-) delete mode 100644 lib/Command/ImportHelper/AImport.php delete mode 100644 lib/Command/ImportHelper/ImportInterface.php delete mode 100644 lib/Command/ImportHelper/TrelloHelper.php create mode 100644 lib/Controller/BoardImportApiController.php create mode 100644 lib/Service/ABoardImportService.php delete mode 100644 lib/Service/AImportService.php create mode 100644 lib/Service/BoardImportCommandService.php create mode 100644 lib/Service/BoardImportService.php rename lib/Service/{TrelloImportService.php => BoardImportTrelloService.php} (55%) rename lib/{Command/ImportHelper => Service}/fixtures/config-trello-schema.json (100%) rename tests/{unit/Command/fixtures => data}/config-trello.json (100%) rename tests/{unit/Command/fixtures => data}/data-trello.json (100%) create mode 100644 tests/unit/Service/BoardImportServiceTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 9ffa71655..63d98159f 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -90,6 +90,8 @@ return [ ['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'], ['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'], + ['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST', 'requirements' => ['apiVersion' => '1.1']], + ['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'], ['name' => 'stack_api#getArchived', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks/archived', 'verb' => 'GET'], diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index ac1ebaab2..2d0cd0678 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -23,40 +23,28 @@ namespace OCA\Deck\Command; -use JsonSchema\Constraints\Constraint; -use JsonSchema\Validator; -use OCA\Deck\Command\ImportHelper\AImport; -use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\BoardImportCommandService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; class BoardImport extends Command { - /** @var string */ - private $system; - private $allowedSystems; - /** @var TrelloHelper */ - private $trelloHelper; + /** @var boardImportCommandService */ + private $boardImportCommandService; public function __construct( - TrelloHelper $trelloHelper + BoardImportCommandService $boardImportCommandService ) { + $this->boardImportCommandService = $boardImportCommandService; parent::__construct(); - $this->trelloHelper = $trelloHelper; } /** * @return void */ protected function configure() { - $allowedSystems = glob(__DIR__ . '/ImportHelper/*Helper.php'); - $this->allowedSystems = array_map(function ($name) { - preg_match('/\/(?\w+)Helper\.php$/', $name, $matches); - return lcfirst($matches['system']); - }, $allowedSystems); + $allowedSystems = $this->boardImportCommandService->getAllowedImportSystems(); $this ->setName('deck:import') ->setDescription('Import data') @@ -64,7 +52,7 @@ class BoardImport extends Command { 'system', null, InputOption::VALUE_REQUIRED, - 'Source system for import. Available options: ' . implode(', ', $this->allowedSystems) . '.', + 'Source system for import. Available options: ' . implode(', ', $allowedSystems) . '.', 'trello' ) ->addOption( @@ -90,89 +78,10 @@ class BoardImport extends Command { * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { - $this->validateSystem($input, $output); - $this->validateConfig($input, $output); - $this->getSystemHelper() - ->validate($input, $output); - } - - protected function validateConfig(InputInterface $input, OutputInterface $output): void { - $configFile = $input->getOption('config'); - if (!is_file($configFile)) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid config json file: ', - 'config.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'config file not found' - ); - } - return $answer; - }); - $configFile = $helper->ask($input, $output, $question); - $input->setOption('config', $configFile); - } - - $config = json_decode(file_get_contents($configFile)); - $schemaPath = __DIR__ . '/ImportHelper/fixtures/config-' . $this->getSystem() . '-schema.json'; - $validator = new Validator(); - $validator->validate( - $config, - (object)['$ref' => 'file://' . realpath($schemaPath)], - Constraint::CHECK_MODE_APPLY_DEFAULTS - ); - if (!$validator->isValid()) { - $output->writeln('Invalid config file'); - $output->writeln(array_map(function ($v) { - return $v['message']; - }, $validator->getErrors())); - $output->writeln('Valid schema:'); - $output->writeln(print_r(file_get_contents($schemaPath), true)); - $input->setOption('config', null); - $this->validateConfig($input, $output); - } - $this->getSystemHelper()->setConfigInstance($config); - } - - private function setSystem(string $system): void { - $this->system = $system; - } - - public function getSystem() { - return $this->system; - } - - /** - * @return AImport - */ - private function getSystemHelper() { - $helper = $this->{$this->system . 'Helper'}; - $helper->setCommand($this); - return $helper; - } - - /** - * @return void - */ - private function validateSystem(InputInterface $input, OutputInterface $output) { - $system = $input->getOption('system'); - if (in_array($system, $this->allowedSystems)) { - $this->setSystem($system); - return; - } - $helper = $this->getHelper('question'); - $question = new ChoiceQuestion( - 'Please inform a source system', - $this->allowedSystems, - 0 - ); - $question->setErrorMessage('System %s is invalid.'); - $system = $helper->ask($input, $output, $question); - $input->setOption('system', $system); - $this->setSystem($system); + $this->boardImportCommandService + ->setInput($input) + ->setOutput($output) + ->validate(); } /** @@ -182,8 +91,11 @@ class BoardImport extends Command { * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->getSystemHelper() - ->import($input, $output); + $this + ->boardImportCommandService + ->setInput($input) + ->setOutput($output) + ->import(); $output->writeln('Done!'); return 0; } diff --git a/lib/Command/ImportHelper/AImport.php b/lib/Command/ImportHelper/AImport.php deleted file mode 100644 index c7ba56cce..000000000 --- a/lib/Command/ImportHelper/AImport.php +++ /dev/null @@ -1,75 +0,0 @@ -trelloImportService = $trelloImportService; - } - - abstract public function validate(InputInterface $input, OutputInterface $output): void; - - abstract public function import(InputInterface $input, OutputInterface $output): void; - - /** - * Define Command instance - * - * @param Command $command - * @return void - */ - public function setCommand(Command $command): void { - $this->command = $command; - } - - /** - * @return BoardImport - */ - public function getCommand() { - return $this->command; - } - - public function setConfigInstance(\stdClass $config) { - $this->trelloImportService->setConfigInstance($config); - } - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void { - $this->trelloImportService->setConfig($configName, $value); - } - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig(string $configName = null) { - return $this->trelloImportService->getConfig($configName); - } -} diff --git a/lib/Command/ImportHelper/ImportInterface.php b/lib/Command/ImportHelper/ImportInterface.php deleted file mode 100644 index f5d68794c..000000000 --- a/lib/Command/ImportHelper/ImportInterface.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Command\ImportHelper; - -use OCA\Deck\Command\BoardImport; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -interface ImportInterface { - /** - * Validate data before run execute method - * - * @param InputInterface $input - * @param OutputInterface $output - * @return void - */ - public function validate(InputInterface $input, OutputInterface $output): void; - - /** - * Run import - * - * @param InputInterface $input - * @param OutputInterface $output - * @return void - */ - public function import(InputInterface $input, OutputInterface $output): void; - - /** - * Define Command instance - * - * @param Command $command - * @return void - */ - public function setCommand(Command $command): void; - - /** - * @return BoardImport - */ - public function getCommand(); - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void; - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig($configName); -} diff --git a/lib/Command/ImportHelper/TrelloHelper.php b/lib/Command/ImportHelper/TrelloHelper.php deleted file mode 100644 index 448f8e629..000000000 --- a/lib/Command/ImportHelper/TrelloHelper.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Command\ImportHelper; - -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; - -class TrelloHelper extends AImport { - public function validate(InputInterface $input, OutputInterface $output): void { - $this->validateData($input, $output); - $this->trelloImportService->validateOwner(); - $this->trelloImportService->validateUsers(); - } - - public function import(InputInterface $input, OutputInterface $output): void { - $this->trelloImportService->setUserId(); - $output->writeln('Importing board...'); - $this->trelloImportService->importBoard(); - $output->writeln('Assign users to board...'); - $this->trelloImportService->assignUsersToBoard(); - $output->writeln('Importing labels...'); - $this->trelloImportService->importLabels(); - $output->writeln('Importing stacks...'); - $this->trelloImportService->importStacks(); - $output->writeln('Importing cards...'); - $this->trelloImportService->importCards(); - } - - private function validateData(InputInterface $input, OutputInterface $output): void { - $filename = $input->getOption('data'); - if (!is_file($filename)) { - $helper = $this->getCommand()->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($input, $output, $question); - $input->setOption('data', $data); - } - $this->trelloImportService->setData(json_decode(file_get_contents($filename))); - if (!$this->trelloImportService->getData()) { - $output->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($input, $output); - } - } -} diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php new file mode 100644 index 000000000..3545a34ab --- /dev/null +++ b/lib/Controller/BoardImportApiController.php @@ -0,0 +1,50 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Controller; + +use OCA\Deck\Service\BoardImportService; +use OCA\Files\Controller\ApiController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; + +class BoardImportApiController extends ApiController { + /** @var BoardImportService */ + private $boardImportService; + + public function __construct( + BoardImportService $boardImportService + ) { + $this->boardImportService = $boardImportService; + } + + /** + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + */ + public function import($system, $config, $data) { + $board = $this->boardImportService->import($system, $config, $data); + return new DataResponse($board, Http::STATUS_OK); + } +} diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php new file mode 100644 index 000000000..6bdd4b1f3 --- /dev/null +++ b/lib/Service/ABoardImportService.php @@ -0,0 +1,53 @@ +boardImportService = $service; + return $this; + } + + public function getImportService(): BoardImportService { + return $this->boardImportService; + } +} diff --git a/lib/Service/AImportService.php b/lib/Service/AImportService.php deleted file mode 100644 index 3ca20226b..000000000 --- a/lib/Service/AImportService.php +++ /dev/null @@ -1,49 +0,0 @@ -config = $config; - } - - /** - * Define a config - * - * @param string $configName - * @param mixed $value - * @return void - */ - public function setConfig(string $configName, $value): void { - if (!$this->config) { - $this->setConfigInstance(new \stdClass); - } - $this->config->$configName = $value; - } - - /** - * Get a config - * - * @param string $configName config name - * @return mixed - */ - public function getConfig(string $configName = null) { - if (!is_object($this->config)) { - return; - } - if (!$configName) { - return $this->config; - } - if (!property_exists($this->config, $configName)) { - return; - } - return $this->config->$configName; - } -} diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php new file mode 100644 index 000000000..b210e9b5e --- /dev/null +++ b/lib/Service/BoardImportCommandService.php @@ -0,0 +1,180 @@ +command = $command; + } + + /** + * @return BoardImport + */ + public function getCommand() { + return $this->command; + } + + public function setInput($input): self { + $this->input = $input; + return $this; + } + + public function getInput(): InputInterface { + return $this->input; + } + + public function setOutput($output): self { + $this->output = $output; + return $this; + } + + public function getOutput(): OutputInterface { + return $this->output; + } + + public function validate(): self { + $this->validateSystem(); + $this->validateConfig(); + $this->validateData(); + return $this; + } + + private function validateConfig(): void { + $configFile = $this->getInput()->getOption('config'); + if (!is_file($configFile)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid config json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'config file not found' + ); + } + return $answer; + }); + $configFile = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('config', $configFile); + } + + $config = json_decode(file_get_contents($configFile)); + $system = $this->getSystem(); + $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; + $validator = new Validator(); + $validator->validate( + $config, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + $this->getOutput()->writeln('Invalid config file'); + $this->getOutput()->writeln(array_map(function ($v) { + return $v['message']; + }, $validator->getErrors())); + $this->getOutput()->writeln('Valid schema:'); + $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); + $this->getInput()->setOption('config', null); + $this->validateConfig($this->getInput(), $this->getOutput()); + } + $this->setConfigInstance($config); + $this->validateOwner(); + } + + /** + * @return void + */ + private function validateSystem(): self { + $system = $this->getInput()->getOption('system'); + if (in_array($system, $this->getAllowedImportSystems())) { + return $this->setSystem($system); + } + $helper = $this->getCommand()->getHelper('question'); + $question = new ChoiceQuestion( + 'Please inform a source system', + $this->allowedSystems, + 0 + ); + $question->setErrorMessage('System %s is invalid.'); + $system = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('system', $system); + return $this->setSystem($system); + } + + private function validateData(): self { + $filename = $this->getInput()->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('data', $data); + } + $this->setData(json_decode(file_get_contents($filename))); + if (!$this->getData()) { + $this->getOutput()->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($this->getInput(), $this->getOutput()); + } + $this->validateUsers(); + return $this; + } + + public function import(): void { + $this->getOutput()->writeln('Importing board...'); + $this->importBoard(); + $this->getOutput()->writeln('Assign users to board...'); + $this->importAcl(); + $this->getOutput()->writeln('Importing labels...'); + $this->importLabels(); + $this->getOutput()->writeln('Importing stacks...'); + $this->importStacks(); + $this->getOutput()->writeln('Importing cards...'); + $this->importCards(); + $this->getOutput()->writeln('Assign cards to labels...'); + $this->assignCardsToLabels(); + $this->getOutput()->writeln('Iporting comments...'); + $this->importComments(); + $this->getOutput()->writeln('Iporting participants...'); + $this->importParticipants(); + } +} diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php new file mode 100644 index 000000000..38bae3155 --- /dev/null +++ b/lib/Service/BoardImportService.php @@ -0,0 +1,363 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Service; + +use JsonSchema\Constraints\Constraint; +use JsonSchema\Validator; +use OCA\Deck\AppInfo\Application; +use OCA\Deck\BadRequestException; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Label; +use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\NotFoundException; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\MessageTooLongException; +use OCP\Comments\NotFoundException as CommentNotFoundException; +use OCP\IDBConnection; +use OCP\IUserManager; + +class BoardImportService { + /** @var IDBConnection */ + protected $dbConn; + /** @var IUserManager */ + private $userManager; + /** @var BoardMapper */ + private $boardMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var LabelMapper */ + private $labelMapper; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var ICommentsManager */ + private $commentsManager; + /** @var string */ + private $system; + /** @var ABoardImportService */ + private $systemInstance; + /** @var string[] */ + private $allowedSystems; + /** + * Data object created from config JSON + * + * @var \stdClass + */ + public $config; + /** + * Data object created from JSON of origin system + * + * @var \stdClass + */ + private $data; + /** @var Board */ + private $board; + + public function __construct( + IDBConnection $dbConn, + IUserManager $userManager, + BoardMapper $boardMapper, + AclMapper $aclMapper, + LabelMapper $labelMapper, + StackMapper $stackMapper, + CardMapper $cardMapper, + ICommentsManager $commentsManager + ) { + $this->dbConn = $dbConn; + $this->userManager = $userManager; + $this->boardMapper = $boardMapper; + $this->aclMapper = $aclMapper; + $this->labelMapper = $labelMapper; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + $this->commentsManager = $commentsManager; + } + + public function import(): void { + $this->validate(); + $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; + $validator = new Validator(); + $validator->validate( + $config, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + throw new BadRequestException('invalid config'); + } + + if (empty($data)) { + throw new BadRequestException('data must be provided'); + } + $this->getImportService()->setData($data); + $this->getImportService()->import(); + // return $newBoard; + } + + public function validate(): self { + if (is_string($system) === false) { + throw new BadRequestException('system must be provided'); + } + + if (!in_array($system, $this->getAllowedImportSystems())) { + throw new BadRequestException('not allowed system'); + } + + if (empty($config)) { + throw new BadRequestException('config must be provided'); + } + return $this; + } + + public function setSystem(string $system): self { + $this->system = $system; + return $this; + } + + public function getSystem() { + return $this->system; + } + + public function getAllowedImportSystems(): array { + if (!$this->allowedSystems) { + $allowedSystems = glob(__DIR__ . '/BoardImport*Service.php'); + $allowedSystems = array_filter($allowedSystems, function($name) { + $name = basename($name); + switch($name) { + case 'ABoardImportService.php': + case 'BoardImportService.php': + case 'BoardImportCommandService.php': + return false; + } + return true; + }); + $this->allowedSystems = array_map(function ($name) { + preg_match('/\/BoardImport(?\w+)Service\.php$/', $name, $matches); + return lcfirst($matches['system']); + }, $allowedSystems); + } + return $this->allowedSystems; + } + + public function getImportSystem(): ABoardImportService { + $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; + if (!is_object($this->systemInstance)) { + $this->systemInstance = \OC::$server->get($systemClass); + $this->systemInstance->setImportService($this); + } + + return $this->systemInstance; + } + + public function importBoard() { + $board = $this->getImportSystem()->getBoard(); + if ($board) { + $this->boardMapper->insert($board); + $this->board = $board; + } + return $this; + } + + public function getBoard(): Board { + return $this->board; + } + + public function importAcl(): self { + $aclList = $this->getImportSystem()->getAclList(); + foreach ($aclList as $acl) { + $this->aclMapper->insert($acl); + } + return $this; + } + + public function importLabels(): self { + $this->getImportSystem()->importLabels(); + return $this; + } + + public function createLabel($title, $color, $boardId): Label { + $label = new Label(); + $label->setTitle($title); + $label->setColor($color); + $label->setBoardId($boardId); + return $this->labelMapper->insert($label); + } + + public function importStacks(): self { + $stack = $this->getImportSystem()->getStacks(); + foreach ($stack as $code => $stack) { + $this->stackMapper->insert($stack); + $this->getImportSystem()->updateStack($code, $stack); + } + return $this; + } + + public function importCards(): self { + $cards = $this->getImportSystem()->getCards(); + foreach ($cards as $code => $card) { + $this->cardMapper->insert($card); + $this->getImportSystem()->updateCard($code, $card); + } + return $this; + } + + public function assignCardToLabel($cardId, $labelId): self { + $this->cardMapper->assignLabel( + $cardId, + $labelId + ); + return $this; + } + + public function assignCardsToLabels(): self { + $this->getImportSystem()->assignCardsToLabels(); + return $this; + } + + public function importComments(): self { + $this->getImportSystem()->importComments(); + return $this; + } + + public function insertComment($cardId, IComment $comment): IComment { + $comment->setObject('deckCard', (string) $cardId); + $comment->setVerb('comment'); + // Check if parent is a comment on the same card + if ($comment->getParentId() !== '0') { + try { + $comment = $this->commentsManager->get($comment->getParentId()); + if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) { + throw new CommentNotFoundException(); + } + } catch (CommentNotFoundException $e) { + throw new BadRequestException('Invalid parent id: The parent comment was not found or belongs to a different card'); + } + } + + try { + $qb = $this->dbConn->getQueryBuilder(); + + $values = [ + 'parent_id' => $qb->createNamedParameter($comment->getParentId()), + 'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()), + 'children_count' => $qb->createNamedParameter($comment->getChildrenCount()), + 'actor_type' => $qb->createNamedParameter($comment->getActorType()), + 'actor_id' => $qb->createNamedParameter($comment->getActorId()), + 'message' => $qb->createNamedParameter($comment->getMessage()), + 'verb' => $qb->createNamedParameter($comment->getVerb()), + 'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'), + 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'), + 'object_type' => $qb->createNamedParameter($comment->getObjectType()), + 'object_id' => $qb->createNamedParameter($comment->getObjectId()), + 'reference_id' => $qb->createNamedParameter($comment->getReferenceId()) + ]; + + $affectedRows = $qb->insert('comments') + ->values($values) + ->execute(); + + if ($affectedRows > 0) { + $comment->setId((string)$qb->getLastInsertId()); + } + return $comment; + } catch (\InvalidArgumentException $e) { + throw new BadRequestException('Invalid input values'); + } catch (CommentNotFoundException $e) { + throw new NotFoundException('Could not create comment.'); + } + } + + public function importParticipants() { + $this->getImportSystem()->importParticipants(); + } + + public function setData(\stdClass $data): self { + $this->data = $data; + return $this; + } + + public function getData() { + return $this->data; + } + + /** + * Define a config + * + * @param string $configName + * @param mixed $value + * @return self + */ + public function setConfig(string $configName, $value): self { + if (!$this->config) { + $this->setConfigInstance(new \stdClass); + } + $this->config->$configName = $value; + return $this; + } + + /** + * Get a config + * + * @param string $configName config name + * @return mixed + */ + public function getConfig(string $configName = null) { + if (!is_object($this->config)) { + return; + } + if (!$configName) { + return $this->config; + } + if (!property_exists($this->config, $configName)) { + return; + } + return $this->config->$configName; + } + + public function setConfigInstance(\stdClass $config): self { + $this->config = $config; + return $this; + } + + public function validateOwner(): self { + $owner = $this->userManager->get($this->getConfig('owner')); + if (!$owner) { + throw new \LogicException('Owner "' . $this->getConfigboardImportService->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); + } + $this->setConfig('owner', $owner); + return $this; + } + + public function validateUsers(): self { + $this->getImportSystem()->validateUsers(); + return $this; + } +} diff --git a/lib/Service/TrelloImportService.php b/lib/Service/BoardImportTrelloService.php similarity index 55% rename from lib/Service/TrelloImportService.php rename to lib/Service/BoardImportTrelloService.php index c09251579..09b96531d 100644 --- a/lib/Service/TrelloImportService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -23,6 +23,7 @@ namespace OCA\Deck\Service; +use OC\Comments\Comment; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\Assignment; @@ -38,9 +39,7 @@ use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -class TrelloImportService extends AImportService { - /** @var BoardService */ - private $boardService; +class BoardImportTrelloService extends ABoardImportService { /** @var LabelService */ private $labelService; /** @var StackMapper */ @@ -57,8 +56,6 @@ class TrelloImportService extends AImportService { private $userManager; /** @var IL10N */ private $l10n; - /** @var Board */ - private $board; /** * Array of stacks * @@ -75,12 +72,6 @@ class TrelloImportService extends AImportService { private $cards = []; /** @var IUser[] */ private $members = []; - /** - * Data object created from JSON of origin system - * - * @var \StdClass - */ - private $data; public function __construct( BoardService $boardService, @@ -104,31 +95,21 @@ class TrelloImportService extends AImportService { $this->l10n = $l10n; } - public function setData(\stdClass $data) { - $this->data = $data; - } - - public function getData() { - return $this->data; - } - - public function validateOwner(): void { - $owner = $this->userManager->get($this->getConfig('owner')); - if (!$owner) { - throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); - } - $this->setConfig('owner', $owner); + public function validate(): ABoardImportService { + $this->boardImportTrelloService->validateOwner(); + $this->boardImportTrelloService->validateUsers(); + return $this; } /** - * @return void + * @return ABoardImportService */ - public function validateUsers() { - if (empty($this->getConfig('uidRelation'))) { - return; + public function validateUsers(): self { + if (empty($this->getImportService()->getConfig('uidRelation'))) { + return $this; } - foreach ($this->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { - $user = array_filter($this->data->members, function ($u) use ($trelloUid) { + foreach ($this->getImportService()->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { + $user = array_filter($this->getImportService()->getData()->members, function ($u) use ($trelloUid) { return $u->username === $trelloUid; }); if (!$user) { @@ -137,29 +118,35 @@ class TrelloImportService extends AImportService { if (!is_string($nextcloudUid)) { throw new \LogicException('User on setting uidRelation must be a string'); } - $this->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); - if (!$this->getConfig('uidRelation')->$trelloUid) { + $this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) { throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); } $user = current($user); - $this->members[$user->id] = $this->getConfig('uidRelation')->$trelloUid; + $this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid; } + return $this; } - public function assignUsersToBoard(): void { + /** + * @return Acl[] + */ + public function getAclList(): array { + $return = []; foreach ($this->members as $member) { - if ($member->getUID() === $this->getConfig('owner')->getUID()) { + if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { continue; } $acl = new Acl(); - $acl->setBoardId($this->board->getId()); + $acl->setBoardId($this->getImportService()->getBoard()->getId()); $acl->setType(Acl::PERMISSION_TYPE_USER); $acl->setParticipant($member->getUID()); $acl->setPermissionEdit(false); $acl->setPermissionShare(false); $acl->setPermissionManage(false); - $this->aclMapper->insert($acl); + $return[] = $acl; } + return $return; } private function checklistItem($item): string { @@ -181,14 +168,17 @@ class TrelloImportService extends AImportService { return $checklist_string; } - public function importCards(): void { + /** + * @return Card[] + */ + public function getCards(): array { $checklists = []; - foreach ($this->data->checklists as $checklist) { + foreach ($this->getImportService()->getData()->checklists as $checklist) { $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); } - $this->data->checklists = $checklists; + $this->getImportService()->getData()->checklists = $checklists; - foreach ($this->data->cards as $trelloCard) { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { $card = new Card(); $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); $card->setLastModified($lastModified->format('Y-m-d H:i:s')); @@ -196,7 +186,7 @@ class TrelloImportService extends AImportService { $card->setDeletedAt($lastModified->format('U')); } if ((count($trelloCard->idChecklists) !== 0)) { - foreach ($this->data->checklists[$trelloCard->id] as $checklist) { + foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) { $trelloCard->desc .= "\n" . $checklist; } } @@ -206,24 +196,25 @@ class TrelloImportService extends AImportService { $card->setStackId($this->stacks[$trelloCard->idList]->getId()); $card->setType('plain'); $card->setOrder($trelloCard->idShort); - $card->setOwner($this->getConfig('owner')->getUID()); + $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) ->format('Y-m-d H:i:s'); $card->setDuedate($duedate); } - $card = $this->cardMapper->insert($card); $this->cards[$trelloCard->id] = $card; - - $this->associateCardToLabels($card, $trelloCard); - $this->importComments($card, $trelloCard); - $this->assignToMember($card, $trelloCard); } + return $this->cards; + } + + public function updateCard($cardTrelloId, Card $card): self { + $this->cards[$cardTrelloId] = $card; + return $this; } /** - * @return void + * @return ABoardImportService */ private function appendAttachmentsToDescription($trelloCard) { if (empty($trelloCard->attachments)) { @@ -236,86 +227,92 @@ class TrelloImportService extends AImportService { $name = $attachment->name === $attachment->url ? null : $attachment->name; $trelloCard->desc .= "| {$attachment->url} | {$name} | {$attachment->date} |\n"; } + return $this; } - private function assignToMember(Card $card, $trelloCard): void { - foreach ($trelloCard->idMembers as $idMember) { - $assignment = new Assignment(); - $assignment->setCardId($card->getId()); - $assignment->setParticipant($this->members[$idMember]->getUID()); - $assignment->setType(Assignment::TYPE_USER); - $assignment = $this->assignmentMapper->insert($assignment); + public function importParticipants(): ABoardImportService { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->idMembers as $idMember) { + $assignment = new Assignment(); + $assignment->setCardId($this->cards[$trelloCard->id]->getId()); + $assignment->setParticipant($this->members[$idMember]->getUID()); + $assignment->setType(Assignment::TYPE_USER); + $assignment = $this->assignmentMapper->insert($assignment); + } } + return $this; } - private function importComments(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { - $comments = array_filter( - $this->data->actions, - function ($a) use ($trelloCard) { - return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + public function importComments(): ABoardImportService { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + $comments = array_filter( + $this->getImportService()->getData()->actions, + function ($a) use ($trelloCard) { + return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + } + ); + foreach ($comments as $trelloComment) { + if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + } else { + $actor = $this->getImportService()->getConfig('owner')->getUID(); + } + $comment = new Comment(); + $comment + ->setActor('users', $actor) + ->setMessage($this->replaceUsernames($trelloComment->data->text), 0) + ->setCreationDateTime( + \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) + ); + $this->getImportService()->insertComment( + $this->cards[$trelloCard->id]->getId(), + $comment + ); } - ); - foreach ($comments as $trelloComment) { - if (!empty($this->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { - $actor = $this->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); - } else { - $actor = $this->getConfig('owner')->getUID(); - } - $message = $this->replaceUsernames($trelloComment->data->text); - $qb = $this->connection->getQueryBuilder(); - - $values = [ - 'parent_id' => $qb->createNamedParameter(0), - 'topmost_parent_id' => $qb->createNamedParameter(0), - 'children_count' => $qb->createNamedParameter(0), - 'actor_type' => $qb->createNamedParameter('users'), - 'actor_id' => $qb->createNamedParameter($actor), - 'message' => $qb->createNamedParameter($message), - 'verb' => $qb->createNamedParameter('comment'), - 'creation_timestamp' => $qb->createNamedParameter( - \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) - ->format('Y-m-d H:i:s') - ), - 'latest_child_timestamp' => $qb->createNamedParameter(null), - 'object_type' => $qb->createNamedParameter('deckCard'), - 'object_id' => $qb->createNamedParameter($card->getId()), - ]; - - $qb->insert('comments') - ->values($values) - ->execute(); } + return $this; } private function replaceUsernames($text) { - foreach ($this->getConfig('uidRelation') as $trello => $nextcloud) { + foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { $text = str_replace($trello, $nextcloud->getUID(), $text); } return $text; } - private function associateCardToLabels(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { - foreach ($trelloCard->labels as $label) { - $this->cardMapper->assignLabel( - $card->getId(), - $this->labels[$label->id]->getId() - ); + public function assignCardsToLabels(): self { + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->labels as $label) { + $this->getImportService()->assignCardToLabel( + $this->cards[$trelloCard->id]->getId(), + $this->labels[$label->id]->getId() + ); + } } + return $this; } - public function importStacks(): void { - $this->stacks = []; - foreach ($this->data->lists as $order => $list) { + /** + * @return Stack[] + */ + public function getStacks(): array { + $return = []; + foreach ($this->getImportService()->getData()->lists as $order => $list) { $stack = new Stack(); if ($list->closed) { $stack->setDeletedAt(time()); } $stack->setTitle($list->name); - $stack->setBoardId($this->board->getId()); + $stack->setBoardId($this->getImportService()->getBoard()->getId()); $stack->setOrder($order + 1); - $stack = $this->stackMapper->insert($stack); - $this->stacks[$list->id] = $stack; + $return[$list->id] = $stack; } + return $return; + } + + public function updateStack($id, $stack): self { + $this->stacks[$id] = $stack; + return $this; } private function translateColor($color): string { @@ -345,45 +342,28 @@ class TrelloImportService extends AImportService { } } - public function importBoard(): void { - $this->board = $this->boardService->create( - $this->data->name, - $this->getConfig('owner')->getUID(), - $this->getConfig('color') - ); + public function getBoard(): Board { + $board = new Board(); + $board->setTitle($this->getImportService()->getData()->name); + $board->setOwner($this->getImportService()->getConfig('owner')->getUID()); + $board->setColor($this->getImportService()->getConfig('color')); + return $board; } - public function importLabels(): void { - $this->labels = []; - foreach ($this->data->labels as $label) { + public function importLabels(): self { + foreach ($this->getImportService()->getData()->labels as $label) { if (empty($label->name)) { $labelTitle = 'Unnamed ' . $label->color . ' label'; } else { $labelTitle = $label->name; } - $newLabel = $this->labelService->create( + $newLabel = $this->getImportService()->createLabel( $labelTitle, $this->translateColor($label->color), - $this->board->getId() + $this->getImportService()->getBoard()->getId() ); $this->labels[$label->id] = $newLabel; } - } - - public function setUserId(): void { - if (!property_exists($this->labelService, 'permissionService')) { - return; - } - $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); - $propertyPermissionService->setAccessible(true); - $permissionService = $propertyPermissionService->getValue($this->labelService); - - if (!property_exists($permissionService, 'userId')) { - return; - } - - $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); - $propertyUserId->setAccessible(true); - $propertyUserId->setValue($permissionService, $this->getConfig('owner')->getUID()); + return $this; } } diff --git a/lib/Command/ImportHelper/fixtures/config-trello-schema.json b/lib/Service/fixtures/config-trello-schema.json similarity index 100% rename from lib/Command/ImportHelper/fixtures/config-trello-schema.json rename to lib/Service/fixtures/config-trello-schema.json diff --git a/tests/unit/Command/fixtures/config-trello.json b/tests/data/config-trello.json similarity index 100% rename from tests/unit/Command/fixtures/config-trello.json rename to tests/data/config-trello.json diff --git a/tests/unit/Command/fixtures/data-trello.json b/tests/data/data-trello.json similarity index 100% rename from tests/unit/Command/fixtures/data-trello.json rename to tests/data/data-trello.json diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index 77eac621b..0d369fbcc 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -24,6 +24,9 @@ namespace OCA\Deck\Command; use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\AImportService; +use OCA\Deck\Service\BoardImportService; +use OCA\Deck\Service\BoardService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -32,14 +35,21 @@ use Symfony\Component\Console\Output\OutputInterface; class BoardImportTest extends \Test\TestCase { /** @var TrelloHelper */ private $trelloHelper; + /** @var BoardImportService */ + private $boardImportService; /** @var BoardImport */ private $boardImport; public function setUp(): void { parent::setUp(); $this->trelloHelper = $this->createMock(TrelloHelper::class); + $this->boardImportService = $this->createMock(BoardImportService::class); + $this->boardImportService + ->method('getAllowedImportSystems') + ->willReturn(['trello']); $this->boardImport = new BoardImport( - $this->trelloHelper + $this->trelloHelper, + $this->boardImportService ); $questionHelper = new QuestionHelper(); $this->boardImport->setHelperSet( @@ -60,8 +70,8 @@ class BoardImportTest extends \Test\TestCase { ) ->will($this->returnValueMap([ ['system', 'trello'], - ['config', __DIR__ . '/fixtures/config-trello.json'], - ['data', __DIR__ . '/fixtures/data-trello.json'] + ['config', __DIR__ . '/../../data/config-trello.json'], + ['data', __DIR__ . '/../../data/data-trello.json'] ])); $output = $this->createMock(OutputInterface::class); @@ -69,6 +79,13 @@ class BoardImportTest extends \Test\TestCase { ->expects($this->once()) ->method('writeLn') ->with('Done!'); + $this->boardImportService + ->method('getSystem') + ->willReturn('trello'); + $importService = $this->createMock(AImportService::class); + $this->boardImportService + ->method('getImportService') + ->willReturn($importService); $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); $actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]); diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php index 006436ed0..25972a7d8 100644 --- a/tests/unit/Command/Helper/TrelloHelperTest.php +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -24,6 +24,8 @@ namespace OCA\Deck\Command; use OCA\Deck\Command\ImportHelper\TrelloHelper; +use OCA\Deck\Service\AImportService; +use OCA\Deck\Service\BoardImportService; use OCA\Deck\Service\TrelloImportService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; @@ -33,6 +35,8 @@ use Symfony\Component\Console\Output\OutputInterface; class TrelloHelperTest extends \Test\TestCase { /** @var TrelloImportService */ private $trelloImportService; + /** @var BoardImportService */ + private $boardImportService; /** @var TrelloHelper */ private $trelloHelper; public function setUp(): void { @@ -42,7 +46,14 @@ class TrelloHelperTest extends \Test\TestCase { $this->trelloImportService ); $questionHelper = new QuestionHelper(); - $command = new BoardImport($this->trelloHelper); + $this->boardImportService = $this->createMock(BoardImportService::class); + $this->boardImportService + ->method('getAllowedImportSystems') + ->willReturn(['trello']); + $command = new BoardImport( + $this->trelloHelper, + $this->boardImportService + ); $command->setHelperSet( new HelperSet([ $questionHelper @@ -61,10 +72,18 @@ class TrelloHelperTest extends \Test\TestCase { ) ->will($this->returnValueMap([ ['system', 'trello'], - ['config', __DIR__ . '/../fixtures/config-trello.json'] + ['config', __DIR__ . '/../../../data/config-trello.json'] ])); $output = $this->createMock(OutputInterface::class); + $this->boardImportService + ->method('getSystem') + ->willReturn('trello'); + $importService = $this->createMock(AImportService::class); + $this->boardImportService + ->method('getImportService') + ->willReturn($importService); + $this->invokePrivate($this->trelloHelper->getCommand(), 'validateSystem', [$input, $output]); $this->invokePrivate($this->trelloHelper->getCommand(), 'validateConfig', [$input, $output]); $actual = $this->trelloHelper->import($input, $output); diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php new file mode 100644 index 000000000..7de0cadd1 --- /dev/null +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -0,0 +1,46 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Deck\Service; + +class BoardImportServiceTest extends \Test\TestCase { + /** @var TrelloImportService */ + private $trelloImportService; + /** @var BoardImportService */ + private $boardImportService; + public function setUp(): void { + $this->trelloImportService = $this->createMock(TrelloImportService::class); + $this->boardImportService = new BoardImportService( + $this->trelloImportService + ); + } + + public function testImportSuccess() { + $config = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $actual = $this->boardImportService->import( + 'trello', + $config, + $data + ); + } +} diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php index 7dba39b5d..be2785378 100644 --- a/tests/unit/Service/BoardServiceTest.php +++ b/tests/unit/Service/BoardServiceTest.php @@ -73,6 +73,8 @@ class BoardServiceTest extends TestCase { private $changeHelper; /** @var IEventDispatcher */ private $eventDispatcher; + /** @var TrelloImportService */ + private $trelloImportService; private $userId = 'admin'; public function setUp(): void { @@ -91,6 +93,7 @@ class BoardServiceTest extends TestCase { $this->activityManager = $this->createMock(ActivityManager::class); $this->changeHelper = $this->createMock(ChangeHelper::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->trelloImportService = $this->createMock(TrelloImportService::class); $this->service = new BoardService( $this->boardMapper, @@ -107,6 +110,7 @@ class BoardServiceTest extends TestCase { $this->activityManager, $this->eventDispatcher, $this->changeHelper, + $this->trelloImportService, $this->userId ); From 39a927de18417a2aeb15415d46bbd0239e213f5c Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Thu, 15 Jul 2021 00:06:55 -0300 Subject: [PATCH 07/21] Clean attachment table Clean code Clean attachment table Signed-off-by: Vitor Mattos --- lib/Service/ABoardImportService.php | 16 ++++++------ lib/Service/BoardImportCommandService.php | 1 - lib/Service/BoardImportService.php | 16 +++++++++--- lib/Service/BoardImportTrelloService.php | 32 +++-------------------- tests/unit/Command/BoardImportTest.php | 1 - 5 files changed, 24 insertions(+), 42 deletions(-) diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 6bdd4b1f3..d085e0e8f 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -21,25 +21,25 @@ abstract class ABoardImportService { /** * @return Stack[] */ - abstract function getStacks(): array; + abstract public function getStacks(): array; /** * @return Card[] */ - abstract function getCards(): array; + abstract public function getCards(): array; - abstract function updateStack(string $id, Stack $stack): self; + abstract public function updateStack(string $id, Stack $stack): self; - abstract function updateCard(string $id, Card $card): self; + abstract public function updateCard(string $id, Card $card): self; - abstract function assignCardsToLabels(): self; + abstract public function importParticipants(): self; - abstract function importParticipants(): self; - - abstract function importComments(): self; + abstract public function importComments(): self; abstract public function importLabels(): self; + abstract public function assignCardsToLabels(): self; + abstract public function validateUsers(): self; public function setImportService($service): self { diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index b210e9b5e..4a4b92096 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -5,7 +5,6 @@ namespace OCA\Deck\Service; use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; use OCA\Deck\Command\BoardImport; -use OCA\Deck\Service\BoardImportService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 38bae3155..fec0f707d 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -28,9 +28,9 @@ use JsonSchema\Validator; use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; -use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; @@ -38,7 +38,6 @@ use OCA\Deck\Db\StackMapper; use OCA\Deck\NotFoundException; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; -use OCP\Comments\MessageTooLongException; use OCP\Comments\NotFoundException as CommentNotFoundException; use OCP\IDBConnection; use OCP\IUserManager; @@ -58,6 +57,8 @@ class BoardImportService { private $stackMapper; /** @var CardMapper */ private $cardMapper; + /** @var AssignmentMapper */ + private $assignmentMapper; /** @var ICommentsManager */ private $commentsManager; /** @var string */ @@ -88,6 +89,7 @@ class BoardImportService { AclMapper $aclMapper, LabelMapper $labelMapper, StackMapper $stackMapper, + AssignmentMapper $assignmentMapper, CardMapper $cardMapper, ICommentsManager $commentsManager ) { @@ -98,6 +100,7 @@ class BoardImportService { $this->labelMapper = $labelMapper; $this->stackMapper = $stackMapper; $this->cardMapper = $cardMapper; + $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; } @@ -149,9 +152,9 @@ class BoardImportService { public function getAllowedImportSystems(): array { if (!$this->allowedSystems) { $allowedSystems = glob(__DIR__ . '/BoardImport*Service.php'); - $allowedSystems = array_filter($allowedSystems, function($name) { + $allowedSystems = array_filter($allowedSystems, function ($name) { $name = basename($name); - switch($name) { + switch ($name) { case 'ABoardImportService.php': case 'BoardImportService.php': case 'BoardImportCommandService.php': @@ -177,6 +180,11 @@ class BoardImportService { return $this->systemInstance; } + public function insertAssignment($assignment): self { + $this->assignmentMapper->insert($assignment); + return $this; + } + public function importBoard() { $board = $this->getImportSystem()->getBoard(); if ($board) { diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloService.php index 09b96531d..c429f1cd8 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -25,33 +25,19 @@ namespace OCA\Deck\Service; use OC\Comments\Comment; use OCA\Deck\Db\Acl; -use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\Assignment; use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; -use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; -use OCA\Deck\Db\StackMapper; -use OCP\IDBConnection; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; class BoardImportTrelloService extends ABoardImportService { - /** @var LabelService */ - private $labelService; - /** @var StackMapper */ - private $stackMapper; - /** @var CardMapper */ - private $cardMapper; /** @var AssignmentMapper */ private $assignmentMapper; - /** @var AclMapper */ - private $aclMapper; - /** @var IDBConnection */ - private $connection; /** @var IUserManager */ private $userManager; /** @var IL10N */ @@ -75,22 +61,12 @@ class BoardImportTrelloService extends ABoardImportService { public function __construct( BoardService $boardService, - LabelService $labelService, - StackMapper $stackMapper, - CardMapper $cardMapper, AssignmentMapper $assignmentMapper, - AclMapper $aclMapper, - IDBConnection $connection, IUserManager $userManager, IL10N $l10n ) { $this->boardService = $boardService; - $this->labelService = $labelService; - $this->stackMapper = $stackMapper; - $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; - $this->aclMapper = $aclMapper; - $this->connection = $connection; $this->userManager = $userManager; $this->l10n = $l10n; } @@ -221,11 +197,11 @@ class BoardImportTrelloService extends ABoardImportService { return; } $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; - $trelloCard->desc .= "| {$this->l10n->t('URL')} | {$this->l10n->t('Name')} | {$this->l10n->t('date')} |\n"; - $trelloCard->desc .= "|---|---|---|\n"; + $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; + $trelloCard->desc .= "|---|---\n"; foreach ($trelloCard->attachments as $attachment) { $name = $attachment->name === $attachment->url ? null : $attachment->name; - $trelloCard->desc .= "| {$attachment->url} | {$name} | {$attachment->date} |\n"; + $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; } return $this; } @@ -237,7 +213,7 @@ class BoardImportTrelloService extends ABoardImportService { $assignment->setCardId($this->cards[$trelloCard->id]->getId()); $assignment->setParticipant($this->members[$idMember]->getUID()); $assignment->setType(Assignment::TYPE_USER); - $assignment = $this->assignmentMapper->insert($assignment); + $this->getImportService()->insertAssignment($assignment); } } return $this; diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index 0d369fbcc..4f36d54bd 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -26,7 +26,6 @@ namespace OCA\Deck\Command; use OCA\Deck\Command\ImportHelper\TrelloHelper; use OCA\Deck\Service\AImportService; use OCA\Deck\Service\BoardImportService; -use OCA\Deck\Service\BoardService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; From 4138953208df28ad3c0af7e76fb43b09a7f350df Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 16 Jul 2021 00:44:45 -0300 Subject: [PATCH 08/21] Changes to make possible implement api endpoint Update documentation Start implementing getSystems route Code to route getSystems Controller to board import Change return Increase coverage Signed-off-by: Vitor Mattos --- appinfo/routes.php | 3 +- docs/API.md | 26 +++ lib/Command/BoardImport.php | 2 + lib/Controller/BoardImportApiController.php | 32 +++- lib/Service/ABoardImportService.php | 6 +- lib/Service/BoardImportCommandService.php | 83 +++++---- lib/Service/BoardImportService.php | 114 +++++++----- lib/Service/BoardImportTrelloService.php | 48 +++-- tests/unit/Command/BoardImportTest.php | 42 ++--- .../unit/Command/Helper/TrelloHelperTest.php | 92 ---------- tests/unit/Service/BoardImportServiceTest.php | 66 +++++-- .../Service/BoardImportTrelloServiceTest.php | 169 ++++++++++++++++++ tests/unit/Service/BoardServiceTest.php | 4 - .../unit/Service/TrelloImportServiceTest.php | 104 ----------- .../BoardImportApiControllerTest.php | 74 ++++++++ 15 files changed, 511 insertions(+), 354 deletions(-) delete mode 100644 tests/unit/Command/Helper/TrelloHelperTest.php create mode 100644 tests/unit/Service/BoardImportTrelloServiceTest.php delete mode 100644 tests/unit/Service/TrelloImportServiceTest.php create mode 100644 tests/unit/controller/BoardImportApiControllerTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 63d98159f..55ffc4513 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -90,7 +90,8 @@ return [ ['name' => 'board_api#deleteAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'], ['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'], - ['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST', 'requirements' => ['apiVersion' => '1.1']], + ['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'], + ['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST'], ['name' => 'stack_api#index', 'url' => '/api/v{apiVersion}/boards/{boardId}/stacks', 'verb' => 'GET'], diff --git a/docs/API.md b/docs/API.md index 00cb8f987..def6499fc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -988,6 +988,32 @@ For now only `deck_file` is supported as an attachment type. ##### 200 Success +### GET /boards/import/getSystems - Import a board + +#### Request parameters + +#### Response + +```json +[ + "trello" +] +``` + +### POST /boards/import - Import a board + +#### Request parameters + +| Parameter | Type | Description | +| ------------ | ------- | --------------------------------------------- | +| system | string | The allowed name of system to import from | +| config | Object | The config object (JSON) | +| data | Object | The data object to import (JSON) | + +#### Response + +##### 200 Success + # OCS API The following endpoints are available through the Nextcloud OCS endpoint, which is available at `/ocs/v2.php/apps/deck/api/v1.0/`. diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 2d0cd0678..a05b35c7f 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -81,6 +81,8 @@ class BoardImport extends Command { $this->boardImportCommandService ->setInput($input) ->setOutput($output) + ->setSystem($input->getOption('system')) + ->setConfigInstance($input->getOption('config')) ->validate(); } diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php index 3545a34ab..506b3f7b6 100644 --- a/lib/Controller/BoardImportApiController.php +++ b/lib/Controller/BoardImportApiController.php @@ -24,18 +24,26 @@ namespace OCA\Deck\Controller; use OCA\Deck\Service\BoardImportService; -use OCA\Files\Controller\ApiController; +use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; class BoardImportApiController extends ApiController { /** @var BoardImportService */ private $boardImportService; + /** @var string */ + private $userId; public function __construct( - BoardImportService $boardImportService + $appName, + IRequest $request, + BoardImportService $boardImportService, + $userId ) { + parent::__construct($appName, $request); $this->boardImportService = $boardImportService; + $this->userId = $userId; } /** @@ -44,7 +52,23 @@ class BoardImportApiController extends ApiController { * @NoCSRFRequired */ public function import($system, $config, $data) { - $board = $this->boardImportService->import($system, $config, $data); - return new DataResponse($board, Http::STATUS_OK); + $this->boardImportService->setSystem($system); + $config = json_decode(json_encode($config)); + $config->owner = $this->userId; + $this->boardImportService->setConfigInstance($config); + $this->boardImportService->setData(json_decode(json_encode($data))); + $this->boardImportService->validate(); + $this->boardImportService->import(); + return new DataResponse($this->boardImportService->getBoard(), Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + */ + public function getAllowedSystems() { + $allowedSystems = $this->boardImportService->getAllowedImportSystems(); + return new DataResponse($allowedSystems, Http::STATUS_OK); } } diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index d085e0e8f..98c654fd9 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -5,6 +5,7 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; +use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; abstract class ABoardImportService { @@ -34,9 +35,10 @@ abstract class ABoardImportService { abstract public function importParticipants(): self; - abstract public function importComments(): self; + abstract public function importComments(); - abstract public function importLabels(): self; + /** @return Label[] */ + abstract public function importLabels(): array; abstract public function assignCardsToLabels(): self; diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index 4a4b92096..8b7b3a416 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -1,10 +1,31 @@ + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ namespace OCA\Deck\Service; -use JsonSchema\Constraints\Constraint; -use JsonSchema\Validator; use OCA\Deck\Command\BoardImport; +use OCA\Deck\Exceptions\ConflictException; +use OCA\Deck\NotFoundException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -60,16 +81,16 @@ class BoardImportCommandService extends BoardImportService { return $this->output; } - public function validate(): self { - $this->validateSystem(); - $this->validateConfig(); + public function validate() { $this->validateData(); - return $this; + parent::validate(); } - private function validateConfig(): void { - $configFile = $this->getInput()->getOption('config'); - if (!is_file($configFile)) { + protected function validateConfig() { + try { + parent::validateConfig(); + return; + } catch (NotFoundException $e) { $helper = $this->getCommand()->getHelper('question'); $question = new Question( 'Please inform a valid config json file: ', @@ -84,50 +105,39 @@ class BoardImportCommandService extends BoardImportService { return $answer; }); $configFile = $helper->ask($this->getInput(), $this->getOutput(), $question); - $this->getInput()->setOption('config', $configFile); - } - - $config = json_decode(file_get_contents($configFile)); - $system = $this->getSystem(); - $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; - $validator = new Validator(); - $validator->validate( - $config, - (object)['$ref' => 'file://' . realpath($schemaPath)], - Constraint::CHECK_MODE_APPLY_DEFAULTS - ); - if (!$validator->isValid()) { + $this->setConfigInstance($configFile); + } catch (ConflictException $e) { $this->getOutput()->writeln('Invalid config file'); $this->getOutput()->writeln(array_map(function ($v) { return $v['message']; - }, $validator->getErrors())); + }, $e->getData())); $this->getOutput()->writeln('Valid schema:'); + $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); $this->getInput()->setOption('config', null); - $this->validateConfig($this->getInput(), $this->getOutput()); + $this->setConfigInstance(''); } - $this->setConfigInstance($config); - $this->validateOwner(); + parent::validateConfig(); + return; } - /** - * @return void - */ - private function validateSystem(): self { - $system = $this->getInput()->getOption('system'); - if (in_array($system, $this->getAllowedImportSystems())) { - return $this->setSystem($system); + protected function validateSystem() { + try { + parent::validateSystem(); + return; + } catch (\Throwable $th) { } $helper = $this->getCommand()->getHelper('question'); $question = new ChoiceQuestion( 'Please inform a source system', - $this->allowedSystems, + $this->getAllowedImportSystems(), 0 ); $question->setErrorMessage('System %s is invalid.'); $system = $helper->ask($this->getInput(), $this->getOutput(), $question); $this->getInput()->setOption('system', $system); - return $this->setSystem($system); + $this->setSystem($system); + return; } private function validateData(): self { @@ -152,9 +162,8 @@ class BoardImportCommandService extends BoardImportService { $this->setData(json_decode(file_get_contents($filename))); if (!$this->getData()) { $this->getOutput()->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($this->getInput(), $this->getOutput()); + $this->validateData(); } - $this->validateUsers(); return $this; } diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index fec0f707d..3dfb562cb 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -34,9 +34,10 @@ use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; -use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException as CommentNotFoundException; use OCP\IDBConnection; @@ -105,39 +106,30 @@ class BoardImportService { } public function import(): void { - $this->validate(); - $schemaPath = __DIR__ . '/fixtures/config-' . $system . '-schema.json'; - $validator = new Validator(); - $validator->validate( - $config, - (object)['$ref' => 'file://' . realpath($schemaPath)], - Constraint::CHECK_MODE_APPLY_DEFAULTS - ); - if (!$validator->isValid()) { - throw new BadRequestException('invalid config'); + try { + $this->importBoard(); + $this->importAcl(); + $this->importLabels(); + $this->importStacks(); + $this->importCards(); + $this->assignCardsToLabels(); + $this->importComments(); + $this->importParticipants(); + } catch (\Throwable $th) { + throw new BadRequestException($th->getMessage()); } - - if (empty($data)) { - throw new BadRequestException('data must be provided'); - } - $this->getImportService()->setData($data); - $this->getImportService()->import(); - // return $newBoard; } - public function validate(): self { - if (is_string($system) === false) { - throw new BadRequestException('system must be provided'); - } + public function validate() { + $this->validateSystem(); + $this->validateConfig(); + $this->validateUsers(); + } - if (!in_array($system, $this->getAllowedImportSystems())) { - throw new BadRequestException('not allowed system'); + protected function validateSystem() { + if (!in_array($this->getSystem(), $this->getAllowedImportSystems())) { + throw new NotFoundException('Invalid system'); } - - if (empty($config)) { - throw new BadRequestException('config must be provided'); - } - return $this; } public function setSystem(string $system): self { @@ -162,10 +154,11 @@ class BoardImportService { } return true; }); - $this->allowedSystems = array_map(function ($name) { + $allowedSystems = array_map(function ($name) { preg_match('/\/BoardImport(?\w+)Service\.php$/', $name, $matches); return lcfirst($matches['system']); }, $allowedSystems); + $this->allowedSystems = array_values($allowedSystems); } return $this->allowedSystems; } @@ -180,6 +173,10 @@ class BoardImportService { return $this->systemInstance; } + public function setImportSystem($instance) { + $this->systemInstance = $instance; + } + public function insertAssignment($assignment): self { $this->assignmentMapper->insert($assignment); return $this; @@ -203,12 +200,14 @@ class BoardImportService { foreach ($aclList as $acl) { $this->aclMapper->insert($acl); } + $this->getBoard()->setAcl($aclList); return $this; } - public function importLabels(): self { - $this->getImportSystem()->importLabels(); - return $this; + public function importLabels(): array { + $labels = $this->getImportSystem()->importLabels(); + $this->getBoard()->setLabels($labels); + return $labels; } public function createLabel($title, $color, $boardId): Label { @@ -219,13 +218,17 @@ class BoardImportService { return $this->labelMapper->insert($label); } - public function importStacks(): self { - $stack = $this->getImportSystem()->getStacks(); - foreach ($stack as $code => $stack) { + /** + * @return Stack[] + */ + public function importStacks(): array { + $stacks = $this->getImportSystem()->getStacks(); + foreach ($stacks as $code => $stack) { $this->stackMapper->insert($stack); $this->getImportSystem()->updateStack($code, $stack); } - return $this; + $this->getBoard()->setStacks(array_values($stacks)); + return $stacks; } public function importCards(): self { @@ -255,7 +258,7 @@ class BoardImportService { return $this; } - public function insertComment($cardId, IComment $comment): IComment { + public function insertComment($cardId, $comment) { $comment->setObject('deckCard', (string) $cardId); $comment->setVerb('comment'); // Check if parent is a comment on the same card @@ -338,10 +341,7 @@ class BoardImportService { * @return mixed */ public function getConfig(string $configName = null) { - if (!is_object($this->config)) { - return; - } - if (!$configName) { + if (!is_object($this->config) || !$configName) { return $this->config; } if (!property_exists($this->config, $configName)) { @@ -350,15 +350,41 @@ class BoardImportService { return $this->config->$configName; } - public function setConfigInstance(\stdClass $config): self { + public function setConfigInstance($config): self { $this->config = $config; return $this; } + protected function validateConfig() { + $config = $this->getConfig(); + if (empty($config)) { + throw new NotFoundException('Please inform a valid config json file'); + } + if (is_string($config)) { + if (!is_file($config)) { + throw new NotFoundException('Please inform a valid config json file'); + } + $config = json_decode(file_get_contents($config)); + } + $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; + $validator = new Validator(); + $newConfig = clone $config; + $validator->validate( + $newConfig, + (object)['$ref' => 'file://' . realpath($schemaPath)], + Constraint::CHECK_MODE_APPLY_DEFAULTS + ); + if (!$validator->isValid()) { + throw new ConflictException('Invalid config file', $validator->getErrors()); + } + $this->setConfigInstance($newConfig); + $this->validateOwner(); + } + public function validateOwner(): self { $owner = $this->userManager->get($this->getConfig('owner')); if (!$owner) { - throw new \LogicException('Owner "' . $this->getConfigboardImportService->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); + throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); } $this->setConfig('owner', $owner); return $this; diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloService.php index c429f1cd8..5f5cb614b 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -24,9 +24,9 @@ namespace OCA\Deck\Service; use OC\Comments\Comment; +use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Assignment; -use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; @@ -36,8 +36,6 @@ use OCP\IUser; use OCP\IUserManager; class BoardImportTrelloService extends ABoardImportService { - /** @var AssignmentMapper */ - private $assignmentMapper; /** @var IUserManager */ private $userManager; /** @var IL10N */ @@ -60,25 +58,15 @@ class BoardImportTrelloService extends ABoardImportService { private $members = []; public function __construct( - BoardService $boardService, - AssignmentMapper $assignmentMapper, IUserManager $userManager, IL10N $l10n ) { - $this->boardService = $boardService; - $this->assignmentMapper = $assignmentMapper; $this->userManager = $userManager; $this->l10n = $l10n; } - public function validate(): ABoardImportService { - $this->boardImportTrelloService->validateOwner(); - $this->boardImportTrelloService->validateUsers(); - return $this; - } - /** - * @return ABoardImportService + * @return self */ public function validateUsers(): self { if (empty($this->getImportService()->getConfig('uidRelation'))) { @@ -91,8 +79,8 @@ class BoardImportTrelloService extends ABoardImportService { if (!$user) { throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); } - if (!is_string($nextcloudUid)) { - throw new \LogicException('User on setting uidRelation must be a string'); + if (!is_string($nextcloudUid) && !is_numeric($nextcloudUid)) { + throw new \LogicException('User on setting uidRelation is invalid'); } $this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) { @@ -170,6 +158,9 @@ class BoardImportTrelloService extends ABoardImportService { $card->setTitle($trelloCard->name); $card->setStackId($this->stacks[$trelloCard->idList]->getId()); + $cardsOnStack = $this->stacks[$trelloCard->idList]->getCards(); + $cardsOnStack[] = $card; + $this->stacks[$trelloCard->idList]->setCards($cardsOnStack); $card->setType('plain'); $card->setOrder($trelloCard->idShort); $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); @@ -184,17 +175,17 @@ class BoardImportTrelloService extends ABoardImportService { return $this->cards; } - public function updateCard($cardTrelloId, Card $card): self { - $this->cards[$cardTrelloId] = $card; + public function updateCard($id, Card $card): self { + $this->cards[$id] = $card; return $this; } /** - * @return ABoardImportService + * @return self */ - private function appendAttachmentsToDescription($trelloCard) { + private function appendAttachmentsToDescription($trelloCard): self { if (empty($trelloCard->attachments)) { - return; + return $this; } $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; @@ -206,9 +197,12 @@ class BoardImportTrelloService extends ABoardImportService { return $this; } - public function importParticipants(): ABoardImportService { + public function importParticipants(): self { foreach ($this->getImportService()->getData()->cards as $trelloCard) { foreach ($trelloCard->idMembers as $idMember) { + if (empty($this->members[$idMember])) { + continue; + } $assignment = new Assignment(); $assignment->setCardId($this->cards[$trelloCard->id]->getId()); $assignment->setParticipant($this->members[$idMember]->getUID()); @@ -219,7 +213,7 @@ class BoardImportTrelloService extends ABoardImportService { return $this; } - public function importComments(): ABoardImportService { + public function importComments() { foreach ($this->getImportService()->getData()->cards as $trelloCard) { $comments = array_filter( $this->getImportService()->getData()->actions, @@ -246,7 +240,6 @@ class BoardImportTrelloService extends ABoardImportService { ); } } - return $this; } private function replaceUsernames($text) { @@ -320,13 +313,16 @@ class BoardImportTrelloService extends ABoardImportService { public function getBoard(): Board { $board = new Board(); + if (!$this->getImportService()->getData()->name) { + throw new BadRequestException('Invalid name of board'); + } $board->setTitle($this->getImportService()->getData()->name); $board->setOwner($this->getImportService()->getConfig('owner')->getUID()); $board->setColor($this->getImportService()->getConfig('color')); return $board; } - public function importLabels(): self { + public function importLabels(): array { foreach ($this->getImportService()->getData()->labels as $label) { if (empty($label->name)) { $labelTitle = 'Unnamed ' . $label->color . ' label'; @@ -340,6 +336,6 @@ class BoardImportTrelloService extends ABoardImportService { ); $this->labels[$label->id] = $newLabel; } - return $this; + return $this->labels; } } diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index 4f36d54bd..e2cdbbfbc 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -23,32 +23,23 @@ namespace OCA\Deck\Command; -use OCA\Deck\Command\ImportHelper\TrelloHelper; -use OCA\Deck\Service\AImportService; -use OCA\Deck\Service\BoardImportService; +use OCA\Deck\Service\BoardImportCommandService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class BoardImportTest extends \Test\TestCase { - /** @var TrelloHelper */ - private $trelloHelper; - /** @var BoardImportService */ - private $boardImportService; + /** @var BoardImportCommandService */ + private $boardImportCommandService; /** @var BoardImport */ private $boardImport; public function setUp(): void { parent::setUp(); - $this->trelloHelper = $this->createMock(TrelloHelper::class); - $this->boardImportService = $this->createMock(BoardImportService::class); - $this->boardImportService - ->method('getAllowedImportSystems') - ->willReturn(['trello']); + $this->boardImportCommandService = $this->createMock(boardImportCommandService::class); $this->boardImport = new BoardImport( - $this->trelloHelper, - $this->boardImportService + $this->boardImportCommandService ); $questionHelper = new QuestionHelper(); $this->boardImport->setHelperSet( @@ -60,33 +51,26 @@ class BoardImportTest extends \Test\TestCase { public function testExecuteWithSuccess() { $input = $this->createMock(InputInterface::class); - - $input->method('getOption') + $input + ->method('getOption') ->withConsecutive( - [$this->equalTo('system')], - [$this->equalTo('config')], - [$this->equalTo('data')] + ['system'], + ['config'] ) ->will($this->returnValueMap([ ['system', 'trello'], - ['config', __DIR__ . '/../../data/config-trello.json'], - ['data', __DIR__ . '/../../data/data-trello.json'] + ['config', null] ])); + $output = $this->createMock(OutputInterface::class); $output ->expects($this->once()) ->method('writeLn') ->with('Done!'); - $this->boardImportService - ->method('getSystem') - ->willReturn('trello'); - $importService = $this->createMock(AImportService::class); - $this->boardImportService - ->method('getImportService') - ->willReturn($importService); - $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); + $actual = $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); + $this->assertNull($actual); $actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]); $this->assertEquals(0, $actual); } diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php deleted file mode 100644 index 25972a7d8..000000000 --- a/tests/unit/Command/Helper/TrelloHelperTest.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Command; - -use OCA\Deck\Command\ImportHelper\TrelloHelper; -use OCA\Deck\Service\AImportService; -use OCA\Deck\Service\BoardImportService; -use OCA\Deck\Service\TrelloImportService; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class TrelloHelperTest extends \Test\TestCase { - /** @var TrelloImportService */ - private $trelloImportService; - /** @var BoardImportService */ - private $boardImportService; - /** @var TrelloHelper */ - private $trelloHelper; - public function setUp(): void { - parent::setUp(); - $this->trelloImportService = $this->createMock(TrelloImportService::class); - $this->trelloHelper = new TrelloHelper( - $this->trelloImportService - ); - $questionHelper = new QuestionHelper(); - $this->boardImportService = $this->createMock(BoardImportService::class); - $this->boardImportService - ->method('getAllowedImportSystems') - ->willReturn(['trello']); - $command = new BoardImport( - $this->trelloHelper, - $this->boardImportService - ); - $command->setHelperSet( - new HelperSet([ - $questionHelper - ]) - ); - $this->trelloHelper->setCommand($command); - } - - public function testImportWithSuccess() { - $input = $this->createMock(InputInterface::class); - - $input->method('getOption') - ->withConsecutive( - [$this->equalTo('system')], - [$this->equalTo('config')] - ) - ->will($this->returnValueMap([ - ['system', 'trello'], - ['config', __DIR__ . '/../../../data/config-trello.json'] - ])); - $output = $this->createMock(OutputInterface::class); - - $this->boardImportService - ->method('getSystem') - ->willReturn('trello'); - $importService = $this->createMock(AImportService::class); - $this->boardImportService - ->method('getImportService') - ->willReturn($importService); - - $this->invokePrivate($this->trelloHelper->getCommand(), 'validateSystem', [$input, $output]); - $this->invokePrivate($this->trelloHelper->getCommand(), 'validateConfig', [$input, $output]); - $actual = $this->trelloHelper->import($input, $output); - $this->assertNull($actual); - } -} diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index 7de0cadd1..d0c55dbf0 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -22,25 +22,69 @@ */ namespace OCA\Deck\Service; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Db\StackMapper; +use OCP\Comments\ICommentsManager; +use OCP\IDBConnection; +use OCP\IUserManager; + class BoardImportServiceTest extends \Test\TestCase { - /** @var TrelloImportService */ - private $trelloImportService; + /** @var IDBConnection */ + protected $dbConn; + /** @var IUserManager */ + private $userManager; + /** @var BoardMapper */ + private $boardMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var LabelMapper */ + private $labelMapper; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var AssignmentMapper */ + private $assignmentMapper; + /** @var ICommentsManager */ + private $commentsManager; /** @var BoardImportService */ private $boardImportService; public function setUp(): void { - $this->trelloImportService = $this->createMock(TrelloImportService::class); + $this->dbConn = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->boardMapper = $this->createMock(BoardMapper::class); + $this->aclMapper = $this->createMock(AclMapper::class); + $this->labelMapper = $this->createMock(LabelMapper::class); + $this->stackMapper = $this->createMock(StackMapper::class); + $this->cardMapper = $this->createMock(AssignmentMapper::class); + $this->assignmentMapper = $this->createMock(CardMapper::class); + $this->commentsManager = $this->createMock(ICommentsManager::class); $this->boardImportService = new BoardImportService( - $this->trelloImportService + $this->dbConn, + $this->userManager, + $this->boardMapper, + $this->aclMapper, + $this->labelMapper, + $this->stackMapper, + $this->cardMapper, + $this->assignmentMapper, + $this->commentsManager ); } public function testImportSuccess() { - $config = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); - $actual = $this->boardImportService->import( - 'trello', - $config, - $data - ); + $importService = $this->createMock(ABoardImportService::class); + $board = new Board(); + $importService + ->method('getBoard') + ->willReturn($board); + $this->boardImportService->setImportSystem($importService); + $actual = $this->boardImportService->import(); + $this->assertNull($actual); } } diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/BoardImportTrelloServiceTest.php new file mode 100644 index 000000000..b33fd42af --- /dev/null +++ b/tests/unit/Service/BoardImportTrelloServiceTest.php @@ -0,0 +1,169 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Deck\Service; + +use OCP\IL10N; +use OCP\IUser; +use OCP\IUserManager; + +class BoardImportTrelloServiceTest extends \Test\TestCase { + /** @var BoardImportTrelloService */ + private $service; + /** @var IUserManager */ + private $userManager; + /** @var IL10N */ + private $l10n; + public function setUp(): void { + $this->userManager = $this->createMock(IUserManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->service = new BoardImportTrelloService( + $this->userManager, + $this->l10n + ); + } + + public function testValidateUsersWithoutUsers() { + $importService = $this->createMock(BoardImportService::class); + $this->service->setImportService($importService); + $actual = $this->service->validateUsers(); + $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + } + + public function testValidateUsersWithInvalidUser() { + $this->expectErrorMessage('Trello user trello_user not found in property "members" of json data'); + $importService = $this->createMock(BoardImportService::class); + $importService + ->method('getConfig') + ->willReturn([ + 'trello_user' => 'nextcloud_user' + ]); + $importService + ->method('getData') + ->willReturn(json_decode( + <<service->setImportService($importService); + $actual = $this->service->validateUsers(); + $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + } + + public function testValidateUsersWithNotStringNextcloud() { + $this->expectErrorMessage('User on setting uidRelation is invalid'); + $importService = $this->createMock(BoardImportService::class); + $importService + ->method('getConfig') + ->willReturn([ + 'trello_user' => [] + ]); + $importService + ->method('getData') + ->willReturn(json_decode( + <<service->setImportService($importService); + $actual = $this->service->validateUsers(); + $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + } + + public function testValidateUsersWithNotFoundUser() { + $this->expectErrorMessage('User on setting uidRelation not found: nextcloud_user'); + $importService = $this->createMock(BoardImportService::class); + $importService + ->method('getConfig') + ->willReturn(json_decode( + <<method('getData') + ->willReturn(json_decode( + <<service->setImportService($importService); + $actual = $this->service->validateUsers(); + $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + } + + public function testValidateUsersWithValidUsers() { + $importService = $this->createMock(BoardImportService::class); + $importService + ->method('getConfig') + ->willReturn(json_decode( + <<method('getData') + ->willReturn(json_decode( + <<createMock(IUser::class); + $this->userManager + // ->expects($this->once()) + ->method('get') + ->with('nextcloud_user') + ->willReturn($fakeUser); + $this->service->setImportService($importService); + $actual = $this->service->validateUsers(); + $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + } +} diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php index be2785378..7dba39b5d 100644 --- a/tests/unit/Service/BoardServiceTest.php +++ b/tests/unit/Service/BoardServiceTest.php @@ -73,8 +73,6 @@ class BoardServiceTest extends TestCase { private $changeHelper; /** @var IEventDispatcher */ private $eventDispatcher; - /** @var TrelloImportService */ - private $trelloImportService; private $userId = 'admin'; public function setUp(): void { @@ -93,7 +91,6 @@ class BoardServiceTest extends TestCase { $this->activityManager = $this->createMock(ActivityManager::class); $this->changeHelper = $this->createMock(ChangeHelper::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->trelloImportService = $this->createMock(TrelloImportService::class); $this->service = new BoardService( $this->boardMapper, @@ -110,7 +107,6 @@ class BoardServiceTest extends TestCase { $this->activityManager, $this->eventDispatcher, $this->changeHelper, - $this->trelloImportService, $this->userId ); diff --git a/tests/unit/Service/TrelloImportServiceTest.php b/tests/unit/Service/TrelloImportServiceTest.php deleted file mode 100644 index d9bc2ed50..000000000 --- a/tests/unit/Service/TrelloImportServiceTest.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * @author Vitor Mattos - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Deck\Service; - -use OCA\Deck\Db\AclMapper; -use OCA\Deck\Db\AssignmentMapper; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\StackMapper; -use OCP\IDBConnection; -use OCP\IL10N; -use OCP\IUserManager; - -class TrelloImportServiceTest extends \Test\TestCase { - /** @var TrelloImportService */ - private $trelloImportService; - /** @var BoardService */ - private $boardService; - /** @var LabelService */ - private $labelService; - /** @var StackMapper */ - private $stackMapper; - /** @var CardMapper */ - private $cardMapper; - /** @var AssignmentMapper */ - private $assignmentMapper; - /** @var AclMapper */ - private $aclMapper; - /** @var IDBConnection */ - private $connection; - /** @var IUserManager */ - private $userManager; - /** @var IL10N */ - private $l10n; - public function setUp(): void { - parent::setUp(); - $this->boardService = $this->createMock(BoardService::class); - $this->labelService = $this->createMock(LabelService::class); - $this->stackMapper = $this->createMock(StackMapper::class); - $this->cardMapper = $this->createMock(CardMapper::class); - $this->assignmentMapper = $this->createMock(AssignmentMapper::class); - $this->aclMapper = $this->createMock(AclMapper::class); - $this->connection = $this->createMock(IDBConnection::class); - $this->userManager = $this->createMock(IUserManager::class); - $this->l10n = $this->createMock(IL10N::class); - $this->trelloImportService = new TrelloImportService( - $this->boardService, - $this->labelService, - $this->stackMapper, - $this->cardMapper, - $this->assignmentMapper, - $this->aclMapper, - $this->connection, - $this->userManager, - $this->l10n - ); - } - - public function testValidateOwnerWithFaliure() { - $owner = $this->createMock(\OCP\IUser::class); - $owner - ->method('getUID') - ->willReturn('admin'); - $this->trelloImportService->setConfig('owner', $owner); - $this->userManager - ->method('get') - ->willReturn(null); - $this->expectErrorMessage('Owner "admin" not found on Nextcloud. Check setting json.'); - $this->trelloImportService->validateOwner(); - } - - public function testValidateOwnerWithSuccess() { - $owner = $this->createMock(\OCP\IUser::class); - $owner - ->method('getUID') - ->willReturn('admin'); - $this->trelloImportService->setConfig('owner', $owner); - $this->userManager - ->method('get') - ->willReturn($owner); - $actual = $this->trelloImportService->validateOwner(); - $this->assertNull($actual); - } -} diff --git a/tests/unit/controller/BoardImportApiControllerTest.php b/tests/unit/controller/BoardImportApiControllerTest.php new file mode 100644 index 000000000..5389b46f1 --- /dev/null +++ b/tests/unit/controller/BoardImportApiControllerTest.php @@ -0,0 +1,74 @@ + + * + * @author Ryan Fletcher + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Deck\Controller; + +use OCA\Deck\Db\Board; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; +use OCA\Deck\Service\BoardImportService; + +class BoardImportApiControllerTest extends \Test\TestCase { + private $appName = 'deck'; + private $userId = 'admin'; + /** @var BoardImportApiController */ + private $controller; + /** @var BoardImportService */ + private $boardImportService; + + public function setUp(): void { + parent::setUp(); + $this->request = $this->createMock(IRequest::class); + $this->boardImportService = $this->createMock(BoardImportService::class); + + $this->controller = new BoardImportApiController( + $this->appName, + $this->request, + $this->boardImportService, + $this->userId + ); + } + + public function testGetAllowedSystems() { + $this->boardImportService + ->method('getAllowedImportSystems') + ->willReturn(['trello']); + $actual = $this->controller->getAllowedSystems(); + $expected = new DataResponse(['trello'], HTTP::STATUS_OK); + $this->assertEquals($expected, $actual); + } + + public function testImport() { + $system = 'trello'; + $config = [ + 'owner' => 'test' + ]; + $data = [ + 'name' => 'test' + ]; + $actual = $this->controller->import($system, $config, $data); + $board = $this->createMock(Board::class); + $this->assertInstanceOf(Board::class, $board); + $this->assertEquals(HTTP::STATUS_OK, $actual->getStatus()); + } +} From e01e4cf1a703cbe8d9fbfef00d079a1da7a86c51 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sat, 17 Jul 2021 08:34:44 -0300 Subject: [PATCH 09/21] Create route to get json schema to validate config Fix visibility Make compatible with php 7.2 Remove returing instance Increase coverage Reduce psalm info Throw exception if system not defined Increment coverage Signed-off-by: Vitor Mattos --- appinfo/routes.php | 1 + docs/API.md | 17 +++ lib/Command/BoardImport.php | 2 +- lib/Controller/BoardImportApiController.php | 20 ++- lib/Service/ABoardImportService.php | 36 ++++-- lib/Service/BoardImportCommandService.php | 47 ++++---- lib/Service/BoardImportService.php | 114 +++++++++++------- lib/Service/BoardImportTrelloService.php | 52 ++++---- tests/unit/Service/BoardImportServiceTest.php | 25 ++++ .../Service/BoardImportTrelloServiceTest.php | 107 +++++++--------- 10 files changed, 245 insertions(+), 176 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 55ffc4513..2e615efab 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -91,6 +91,7 @@ return [ ['name' => 'board_api#updateAcl', 'url' => '/api/v{apiVersion}/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'], ['name' => 'board_import_api#getAllowedSystems', 'url' => '/api/v{apiVersion}/boards/import/getSystems','verb' => 'GET'], + ['name' => 'board_import_api#getConfigSchema', 'url' => '/api/v{apiVersion}/boards/import/config/schema/{name}','verb' => 'GET'], ['name' => 'board_import_api#import', 'url' => '/api/v{apiVersion}/boards/import','verb' => 'POST'], diff --git a/docs/API.md b/docs/API.md index def6499fc..d3191b96a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -992,6 +992,23 @@ For now only `deck_file` is supported as an attachment type. #### Request parameters +| Parameter | Type | Description | +| ------------ | ------- | --------------------------------------------- | +| system | Integer | The system name. Example: trello | + +#### Response + +Make a request to see the json schema of system + +```json +{ +} +``` + +### GET /boards/import/config/system/{schema} - Import a board + +#### Request parameters + #### Response ```json diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index a05b35c7f..ba4ad8a72 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -30,7 +30,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class BoardImport extends Command { - /** @var boardImportCommandService */ + /** @var BoardImportCommandService */ private $boardImportCommandService; public function __construct( diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php index 506b3f7b6..febad393b 100644 --- a/lib/Controller/BoardImportApiController.php +++ b/lib/Controller/BoardImportApiController.php @@ -36,10 +36,10 @@ class BoardImportApiController extends ApiController { private $userId; public function __construct( - $appName, + string $appName, IRequest $request, BoardImportService $boardImportService, - $userId + string $userId ) { parent::__construct($appName, $request); $this->boardImportService = $boardImportService; @@ -51,7 +51,7 @@ class BoardImportApiController extends ApiController { * @CORS * @NoCSRFRequired */ - public function import($system, $config, $data) { + public function import(string $system, array $config, array $data): DataResponse { $this->boardImportService->setSystem($system); $config = json_decode(json_encode($config)); $config->owner = $this->userId; @@ -67,8 +67,20 @@ class BoardImportApiController extends ApiController { * @CORS * @NoCSRFRequired */ - public function getAllowedSystems() { + public function getAllowedSystems(): DataResponse { $allowedSystems = $this->boardImportService->getAllowedImportSystems(); return new DataResponse($allowedSystems, Http::STATUS_OK); } + + /** + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + */ + public function getConfigSchema(string $name): DataResponse { + $this->boardImportService->setSystem($name); + $this->boardImportService->validateSystem(); + $jsonSchemaPath = json_decode(file_get_contents($this->boardImportService->getJsonSchemaPath())); + return new DataResponse($jsonSchemaPath, Http::STATUS_OK); + } } diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 98c654fd9..ef36fb8e3 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -1,4 +1,25 @@ + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ namespace OCA\Deck\Service; @@ -29,24 +50,23 @@ abstract class ABoardImportService { */ abstract public function getCards(): array; - abstract public function updateStack(string $id, Stack $stack): self; + abstract public function updateStack(string $id, Stack $stack): void; - abstract public function updateCard(string $id, Card $card): self; + abstract public function updateCard(string $id, Card $card): void; - abstract public function importParticipants(): self; + abstract public function importParticipants(): void; - abstract public function importComments(); + abstract public function importComments(): void; /** @return Label[] */ abstract public function importLabels(): array; - abstract public function assignCardsToLabels(): self; + abstract public function assignCardsToLabels(): void; - abstract public function validateUsers(): self; + abstract public function validateUsers(): void; - public function setImportService($service): self { + public function setImportService(BoardImportService $service): void { $this->boardImportService = $service; - return $this; } public function getImportService(): BoardImportService { diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index 8b7b3a416..d286b8a30 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -23,7 +23,6 @@ namespace OCA\Deck\Service; -use OCA\Deck\Command\BoardImport; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; use Symfony\Component\Console\Command\Command; @@ -33,18 +32,21 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; class BoardImportCommandService extends BoardImportService { - /** @var Command */ - private $command; - /** @var InputInterface */ - private $input; - /** @var OutputInterface */ - private $output; /** - * Data object created from config JSON - * - * @var \StdClass + * @var Command + * @psalm-suppress PropertyNotSetInConstructor */ - public $config; + private $command; + /** + * @var InputInterface + * @psalm-suppress PropertyNotSetInConstructor + */ + private $input; + /** + * @var OutputInterface + * @psalm-suppress PropertyNotSetInConstructor + */ + private $output; /** * Define Command instance @@ -56,14 +58,11 @@ class BoardImportCommandService extends BoardImportService { $this->command = $command; } - /** - * @return BoardImport - */ - public function getCommand() { + public function getCommand(): Command { return $this->command; } - public function setInput($input): self { + public function setInput(InputInterface $input): self { $this->input = $input; return $this; } @@ -72,7 +71,7 @@ class BoardImportCommandService extends BoardImportService { return $this->input; } - public function setOutput($output): self { + public function setOutput(OutputInterface $output): self { $this->output = $output; return $this; } @@ -81,12 +80,12 @@ class BoardImportCommandService extends BoardImportService { return $this->output; } - public function validate() { + public function validate(): void { $this->validateData(); parent::validate(); } - protected function validateConfig() { + protected function validateConfig(): void { try { parent::validateConfig(); return; @@ -96,7 +95,7 @@ class BoardImportCommandService extends BoardImportService { 'Please inform a valid config json file: ', 'config.json' ); - $question->setValidator(function ($answer) { + $question->setValidator(function (string $answer) { if (!is_file($answer)) { throw new \RuntimeException( 'config file not found' @@ -108,7 +107,7 @@ class BoardImportCommandService extends BoardImportService { $this->setConfigInstance($configFile); } catch (ConflictException $e) { $this->getOutput()->writeln('Invalid config file'); - $this->getOutput()->writeln(array_map(function ($v) { + $this->getOutput()->writeln(array_map(function (array $v): string { return $v['message']; }, $e->getData())); $this->getOutput()->writeln('Valid schema:'); @@ -121,7 +120,7 @@ class BoardImportCommandService extends BoardImportService { return; } - protected function validateSystem() { + public function validateSystem(): void { try { parent::validateSystem(); return; @@ -142,13 +141,13 @@ class BoardImportCommandService extends BoardImportService { private function validateData(): self { $filename = $this->getInput()->getOption('data'); - if (!is_file($filename)) { + if (!is_string($filename) || empty($filename) || !is_file($filename)) { $helper = $this->getCommand()->getHelper('question'); $question = new Question( 'Please inform a valid data json file: ', 'data.json' ); - $question->setValidator(function ($answer) { + $question->setValidator(function (string $answer) { if (!is_file($answer)) { throw new \RuntimeException( 'Data file not found' diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 3dfb562cb..49a8bc4f4 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -25,9 +25,11 @@ namespace OCA\Deck\Service; use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; +use OC\Comments\Comment; use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Assignment; use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; @@ -38,6 +40,7 @@ use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; +use OCP\AppFramework\Db\Entity; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException as CommentNotFoundException; use OCP\IDBConnection; @@ -63,24 +66,29 @@ class BoardImportService { /** @var ICommentsManager */ private $commentsManager; /** @var string */ - private $system; - /** @var ABoardImportService */ + private $system = ''; + /** @var null|ABoardImportService */ private $systemInstance; /** @var string[] */ - private $allowedSystems; + private $allowedSystems = []; /** * Data object created from config JSON * * @var \stdClass + * @psalm-suppress PropertyNotSetInConstructor */ public $config; /** * Data object created from JSON of origin system * * @var \stdClass + * @psalm-suppress PropertyNotSetInConstructor */ private $data; - /** @var Board */ + /** + * @var Board + * @psalm-suppress PropertyNotSetInConstructor + */ private $board; public function __construct( @@ -103,6 +111,7 @@ class BoardImportService { $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; + $this->setData(new \stdClass()); } public function import(): void { @@ -120,31 +129,35 @@ class BoardImportService { } } - public function validate() { + public function validate(): void { $this->validateSystem(); $this->validateConfig(); $this->validateUsers(); } - protected function validateSystem() { + public function validateSystem(): void { if (!in_array($this->getSystem(), $this->getAllowedImportSystems())) { throw new NotFoundException('Invalid system'); } } - public function setSystem(string $system): self { + /** + * @param mixed $system + * @return self + */ + public function setSystem($system): self { $this->system = $system; return $this; } - public function getSystem() { + public function getSystem(): string { return $this->system; } public function getAllowedImportSystems(): array { if (!$this->allowedSystems) { $allowedSystems = glob(__DIR__ . '/BoardImport*Service.php'); - $allowedSystems = array_filter($allowedSystems, function ($name) { + $allowedSystems = array_filter($allowedSystems, function (string $name) { $name = basename($name); switch ($name) { case 'ABoardImportService.php': @@ -165,6 +178,9 @@ class BoardImportService { public function getImportSystem(): ABoardImportService { $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; + if (!$this->getSystem()) { + throw new NotFoundException('System to import not found'); + } if (!is_object($this->systemInstance)) { $this->systemInstance = \OC::$server->get($systemClass); $this->systemInstance->setImportService($this); @@ -173,22 +189,21 @@ class BoardImportService { return $this->systemInstance; } - public function setImportSystem($instance) { + public function setImportSystem(ABoardImportService $instance): void { $this->systemInstance = $instance; } - public function insertAssignment($assignment): self { + public function insertAssignment(Assignment $assignment): self { $this->assignmentMapper->insert($assignment); return $this; } - public function importBoard() { + public function importBoard(): void { $board = $this->getImportSystem()->getBoard(); if ($board) { $this->boardMapper->insert($board); $this->board = $board; } - return $this; } public function getBoard(): Board { @@ -210,7 +225,7 @@ class BoardImportService { return $labels; } - public function createLabel($title, $color, $boardId): Label { + public function createLabel(string $title, string $color, int $boardId): Entity { $label = new Label(); $label->setTitle($title); $label->setColor($color); @@ -240,6 +255,11 @@ class BoardImportService { return $this; } + /** + * @param mixed $cardId + * @param mixed $labelId + * @return self + */ public function assignCardToLabel($cardId, $labelId): self { $this->cardMapper->assignLabel( $cardId, @@ -248,18 +268,16 @@ class BoardImportService { return $this; } - public function assignCardsToLabels(): self { + public function assignCardsToLabels(): void { $this->getImportSystem()->assignCardsToLabels(); - return $this; } - public function importComments(): self { + public function importComments(): void { $this->getImportSystem()->importComments(); - return $this; } - public function insertComment($cardId, $comment) { - $comment->setObject('deckCard', (string) $cardId); + public function insertComment(string $cardId, Comment $comment): void { + $comment->setObject('deckCard', $cardId); $comment->setVerb('comment'); // Check if parent is a comment on the same card if ($comment->getParentId() !== '0') { @@ -298,7 +316,6 @@ class BoardImportService { if ($affectedRows > 0) { $comment->setId((string)$qb->getLastInsertId()); } - return $comment; } catch (\InvalidArgumentException $e) { throw new BadRequestException('Invalid input values'); } catch (CommentNotFoundException $e) { @@ -306,16 +323,15 @@ class BoardImportService { } } - public function importParticipants() { + public function importParticipants(): void { $this->getImportSystem()->importParticipants(); } - public function setData(\stdClass $data): self { + final public function setData(\stdClass $data): void { $this->data = $data; - return $this; } - public function getData() { + public function getData(): \stdClass { return $this->data; } @@ -324,14 +340,13 @@ class BoardImportService { * * @param string $configName * @param mixed $value - * @return self + * @return void */ - public function setConfig(string $configName, $value): self { - if (!$this->config) { + public function setConfig(string $configName, $value): void { + if (empty((array) $this->config)) { $this->setConfigInstance(new \stdClass); } $this->config->$configName = $value; - return $this; } /** @@ -341,32 +356,37 @@ class BoardImportService { * @return mixed */ public function getConfig(string $configName = null) { - if (!is_object($this->config) || !$configName) { - return $this->config; - } if (!property_exists($this->config, $configName)) { return; } return $this->config->$configName; } + /** + * @param mixed $config + * @return self + */ public function setConfigInstance($config): self { - $this->config = $config; - return $this; - } - - protected function validateConfig() { - $config = $this->getConfig(); - if (empty($config)) { - throw new NotFoundException('Please inform a valid config json file'); - } if (is_string($config)) { if (!is_file($config)) { throw new NotFoundException('Please inform a valid config json file'); } $config = json_decode(file_get_contents($config)); + if (!is_object($config)) { + throw new NotFoundException('Please inform a valid config json file'); + } } - $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; + $this->config = $config; + return $this; + } + + public function getConfigInstance(): \stdClass { + return $this->config; + } + + protected function validateConfig(): void { + $config = $this->getConfigInstance(); + $schemaPath = $this->getJsonSchemaPath(); $validator = new Validator(); $newConfig = clone $config; $validator->validate( @@ -381,17 +401,19 @@ class BoardImportService { $this->validateOwner(); } - public function validateOwner(): self { + public function getJsonSchemaPath(): string { + return __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; + } + + public function validateOwner(): void { $owner = $this->userManager->get($this->getConfig('owner')); if (!$owner) { throw new \LogicException('Owner "' . $this->getConfig('owner')->getUID() . '" not found on Nextcloud. Check setting json.'); } $this->setConfig('owner', $owner); - return $this; } - public function validateUsers(): self { + public function validateUsers(): void { $this->getImportSystem()->validateUsers(); - return $this; } } diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloService.php index 5f5cb614b..adf8b0abe 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -31,6 +31,7 @@ use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; +use OCP\AppFramework\Db\Entity; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -47,9 +48,9 @@ class BoardImportTrelloService extends ABoardImportService { */ private $stacks = []; /** - * Array of labels + * Array of Labels * - * @var Label[] + * @var Label[]|Entity[] */ private $labels = []; /** @var Card[] */ @@ -65,15 +66,12 @@ class BoardImportTrelloService extends ABoardImportService { $this->l10n = $l10n; } - /** - * @return self - */ - public function validateUsers(): self { + public function validateUsers(): void { if (empty($this->getImportService()->getConfig('uidRelation'))) { - return $this; + return; } foreach ($this->getImportService()->getConfig('uidRelation') as $trelloUid => $nextcloudUid) { - $user = array_filter($this->getImportService()->getData()->members, function ($u) use ($trelloUid) { + $user = array_filter($this->getImportService()->getData()->members, function (\stdClass $u) use ($trelloUid) { return $u->username === $trelloUid; }); if (!$user) { @@ -82,6 +80,7 @@ class BoardImportTrelloService extends ABoardImportService { if (!is_string($nextcloudUid) && !is_numeric($nextcloudUid)) { throw new \LogicException('User on setting uidRelation is invalid'); } + $nextcloudUid = (string) $nextcloudUid; $this->getImportService()->getConfig('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); if (!$this->getImportService()->getConfig('uidRelation')->$trelloUid) { throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); @@ -89,7 +88,6 @@ class BoardImportTrelloService extends ABoardImportService { $user = current($user); $this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid; } - return $this; } /** @@ -113,7 +111,7 @@ class BoardImportTrelloService extends ABoardImportService { return $return; } - private function checklistItem($item): string { + private function checklistItem(\stdClass $item): string { if (($item->state == 'incomplete')) { $string_start = '- [ ]'; } else { @@ -123,7 +121,7 @@ class BoardImportTrelloService extends ABoardImportService { return $check_item_string; } - private function formulateChecklistText($checklist): string { + private function formulateChecklistText(\stdClass $checklist): string { $checklist_string = "\n\n## {$checklist->name}\n"; foreach ($checklist->checkItems as $item) { $checklist_item_string = $this->checklistItem($item); @@ -175,17 +173,13 @@ class BoardImportTrelloService extends ABoardImportService { return $this->cards; } - public function updateCard($id, Card $card): self { + public function updateCard(string $id, Card $card): void { $this->cards[$id] = $card; - return $this; } - /** - * @return self - */ - private function appendAttachmentsToDescription($trelloCard): self { + private function appendAttachmentsToDescription(\stdClass $trelloCard): void { if (empty($trelloCard->attachments)) { - return $this; + return; } $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; @@ -194,10 +188,9 @@ class BoardImportTrelloService extends ABoardImportService { $name = $attachment->name === $attachment->url ? null : $attachment->name; $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; } - return $this; } - public function importParticipants(): self { + public function importParticipants(): void { foreach ($this->getImportService()->getData()->cards as $trelloCard) { foreach ($trelloCard->idMembers as $idMember) { if (empty($this->members[$idMember])) { @@ -210,14 +203,13 @@ class BoardImportTrelloService extends ABoardImportService { $this->getImportService()->insertAssignment($assignment); } } - return $this; } - public function importComments() { + public function importComments(): void { foreach ($this->getImportService()->getData()->cards as $trelloCard) { $comments = array_filter( $this->getImportService()->getData()->actions, - function ($a) use ($trelloCard) { + function (\stdClass $a) use ($trelloCard) { return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; } ); @@ -235,21 +227,21 @@ class BoardImportTrelloService extends ABoardImportService { \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) ); $this->getImportService()->insertComment( - $this->cards[$trelloCard->id]->getId(), + (string) $this->cards[$trelloCard->id]->getId(), $comment ); } } } - private function replaceUsernames($text) { + private function replaceUsernames(string $text): string { foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { $text = str_replace($trello, $nextcloud->getUID(), $text); } return $text; } - public function assignCardsToLabels(): self { + public function assignCardsToLabels(): void { foreach ($this->getImportService()->getData()->cards as $trelloCard) { foreach ($trelloCard->labels as $label) { $this->getImportService()->assignCardToLabel( @@ -258,7 +250,6 @@ class BoardImportTrelloService extends ABoardImportService { ); } } - return $this; } /** @@ -279,12 +270,11 @@ class BoardImportTrelloService extends ABoardImportService { return $return; } - public function updateStack($id, $stack): self { + public function updateStack(string $id, Stack $stack): void { $this->stacks[$id] = $stack; - return $this; } - private function translateColor($color): string { + private function translateColor(string $color): string { switch ($color) { case 'red': return 'ff0000'; @@ -313,7 +303,7 @@ class BoardImportTrelloService extends ABoardImportService { public function getBoard(): Board { $board = new Board(); - if (!$this->getImportService()->getData()->name) { + if (empty($this->getImportService()->getData()->name)) { throw new BadRequestException('Invalid name of board'); } $board->setTitle($this->getImportService()->getData()->name); diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index d0c55dbf0..a24022370 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -31,6 +31,7 @@ use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\StackMapper; use OCP\Comments\ICommentsManager; use OCP\IDBConnection; +use OCP\IUser; use OCP\IUserManager; class BoardImportServiceTest extends \Test\TestCase { @@ -83,8 +84,32 @@ class BoardImportServiceTest extends \Test\TestCase { $importService ->method('getBoard') ->willReturn($board); + $this->boardImportService->setSystem('trello'); $this->boardImportService->setImportSystem($importService); $actual = $this->boardImportService->import(); $this->assertNull($actual); } + + public function testImportBoard() { + $this->boardImportService->setSystem('trello'); + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $this->boardImportService->setData($data); + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $this->boardImportService->setConfigInstance($configInstance); + + $owner = $this->createMock(IUser::class); + $owner + ->method('getUID') + ->willReturn('owner'); + $this->userManager + ->method('get') + ->willReturn($owner); + $this->boardImportService->validateOwner(); + $actual = $this->boardImportService->importBoard(); + $this->assertNull($actual); + $board = $this->boardImportService->getBoard(); + $this->assertEquals('Test Board Name', $board->getTitle()); + $this->assertEquals('owner', $board->getOwner()); + $this->assertEquals('0800fd', $board->getColor()); + } } diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/BoardImportTrelloServiceTest.php index b33fd42af..dbfbcd7eb 100644 --- a/tests/unit/Service/BoardImportTrelloServiceTest.php +++ b/tests/unit/Service/BoardImportTrelloServiceTest.php @@ -22,6 +22,7 @@ */ namespace OCA\Deck\Service; +use OCA\Deck\Db\Board; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -46,7 +47,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { $importService = $this->createMock(BoardImportService::class); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); - $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + $this->assertNull($actual); } public function testValidateUsersWithInvalidUser() { @@ -59,17 +60,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { ]); $importService ->method('getData') - ->willReturn(json_decode( - <<willReturn(json_decode('{"members": [{"username": "othre_trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); $this->assertInstanceOf(BoardImportTrelloService::class, $actual); @@ -85,17 +76,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { ]); $importService ->method('getData') - ->willReturn(json_decode( - <<willReturn(json_decode('{"members": [{"username": "trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); $this->assertInstanceOf(BoardImportTrelloService::class, $actual); @@ -106,26 +87,10 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { $importService = $this->createMock(BoardImportService::class); $importService ->method('getConfig') - ->willReturn(json_decode( - <<willReturn(json_decode('{"trello_user": "nextcloud_user"}')); $importService ->method('getData') - ->willReturn(json_decode( - <<willReturn(json_decode('{"members": [{"username": "trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); $this->assertInstanceOf(BoardImportTrelloService::class, $actual); @@ -135,35 +100,53 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { $importService = $this->createMock(BoardImportService::class); $importService ->method('getConfig') - ->willReturn(json_decode( - <<willReturn(json_decode('{"trello_user": "nextcloud_user"}')); $importService ->method('getData') - ->willReturn(json_decode( - <<willReturn(json_decode('{"members": [{"id": "fakeid", "username": "trello_user"}]}')); $fakeUser = $this->createMock(IUser::class); $this->userManager - // ->expects($this->once()) ->method('get') ->with('nextcloud_user') ->willReturn($fakeUser); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); - $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + $this->assertNull($actual); + } + + public function testGetBoardWithSuccess() { + $importService = $this->createMock(BoardImportService::class); + $owner = $this->createMock(IUser::class); + $owner + ->method('getUID') + ->willReturn('owner'); + $importService + ->method('getConfig') + ->withConsecutive( + ['owner'], + ['color'] + )->willReturnonConsecutiveCalls( + $owner, + '000000' + ); + $importService + ->method('getData') + ->willReturn(json_decode('{"name": "test"}')); + $this->service->setImportService($importService); + $actual = $this->service->getBoard(); + $this->assertInstanceOf(Board::class, $actual); + $this->assertEquals('test', $actual->getTitle()); + $this->assertEquals('owner', $actual->getOwner()); + $this->assertEquals('000000', $actual->getColor()); + } + + public function testGetBoardWithInvalidName() { + $this->expectErrorMessage('Invalid name of board'); + $importService = $this->createMock(BoardImportService::class); + $importService + ->method('getData') + ->willReturn(new \stdClass); + $this->service->setImportService($importService); + $this->service->getBoard(); } } From 6714c89220acb62a4295d94e7e69adc9a3a1329b Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 18 Jul 2021 10:11:39 -0300 Subject: [PATCH 10/21] Remove interact from command and implement bootstrap method Signed-off-by: Vitor Mattos --- lib/Command/BoardImport.php | 15 +--- lib/Controller/BoardImportApiController.php | 1 - lib/Service/ABoardImportService.php | 7 +- lib/Service/BoardImportCommandService.php | 83 ++++++++++--------- lib/Service/BoardImportService.php | 33 +++----- lib/Service/BoardImportTrelloService.php | 4 + tests/unit/Service/BoardImportServiceTest.php | 37 +++++---- 7 files changed, 91 insertions(+), 89 deletions(-) diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index ba4ad8a72..2c825ae9e 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -72,20 +72,6 @@ class BoardImport extends Command { ; } - /** - * @inheritDoc - * - * @return void - */ - protected function interact(InputInterface $input, OutputInterface $output) { - $this->boardImportCommandService - ->setInput($input) - ->setOutput($output) - ->setSystem($input->getOption('system')) - ->setConfigInstance($input->getOption('config')) - ->validate(); - } - /** * @param InputInterface $input * @param OutputInterface $output @@ -97,6 +83,7 @@ class BoardImport extends Command { ->boardImportCommandService ->setInput($input) ->setOutput($output) + ->setCommand($this) ->import(); $output->writeln('Done!'); return 0; diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php index febad393b..b6c32183f 100644 --- a/lib/Controller/BoardImportApiController.php +++ b/lib/Controller/BoardImportApiController.php @@ -57,7 +57,6 @@ class BoardImportApiController extends ApiController { $config->owner = $this->userId; $this->boardImportService->setConfigInstance($config); $this->boardImportService->setData(json_decode(json_encode($data))); - $this->boardImportService->validate(); $this->boardImportService->import(); return new DataResponse($this->boardImportService->getBoard(), Http::STATUS_OK); } diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index ef36fb8e3..614c31225 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -63,7 +63,12 @@ abstract class ABoardImportService { abstract public function assignCardsToLabels(): void; - abstract public function validateUsers(): void; + /** + * Configure import service + * + * @return void + */ + abstract public function bootstrap(): void; public function setImportService(BoardImportService $service): void { $this->boardImportService = $service; diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index d286b8a30..bff49e5a0 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -48,14 +48,9 @@ class BoardImportCommandService extends BoardImportService { */ private $output; - /** - * Define Command instance - * - * @param Command $command - * @return void - */ - public function setCommand(Command $command): void { + public function setCommand(Command $command): self { $this->command = $command; + return $this; } public function getCommand(): Command { @@ -80,13 +75,19 @@ class BoardImportCommandService extends BoardImportService { return $this->output; } - public function validate(): void { - $this->validateData(); - parent::validate(); - } - protected function validateConfig(): void { try { + $config = $this->getInput()->getOption('config'); + if (is_string($config)) { + if (!is_file($config)) { + throw new NotFoundException('Please inform a valid config json file'); + } + $config = json_decode(file_get_contents($config)); + if (!$config instanceof \stdClass) { + throw new NotFoundException('Please inform a valid config json file'); + } + $this->setConfigInstance($config); + } parent::validateConfig(); return; } catch (NotFoundException $e) { @@ -104,7 +105,7 @@ class BoardImportCommandService extends BoardImportService { return $answer; }); $configFile = $helper->ask($this->getInput(), $this->getOutput(), $question); - $this->setConfigInstance($configFile); + $config = $this->getInput()->setOption('config', $configFile); } catch (ConflictException $e) { $this->getOutput()->writeln('Invalid config file'); $this->getOutput()->writeln(array_map(function (array $v): string { @@ -114,7 +115,6 @@ class BoardImportCommandService extends BoardImportService { $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); $this->getInput()->setOption('config', null); - $this->setConfigInstance(''); } parent::validateConfig(); return; @@ -139,34 +139,41 @@ class BoardImportCommandService extends BoardImportService { return; } - private function validateData(): self { - $filename = $this->getInput()->getOption('data'); - if (!is_string($filename) || empty($filename) || !is_file($filename)) { - $helper = $this->getCommand()->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function (string $answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($this->getInput(), $this->getOutput(), $question); - $this->getInput()->setOption('data', $data); + protected function validateData(): void { + $data = $this->getInput()->getOption('data'); + if (is_string($data)) { + $data = json_decode(file_get_contents($data)); + if ($data instanceof \stdClass) { + $this->setData($data); + return; + } } - $this->setData(json_decode(file_get_contents($filename))); - if (!$this->getData()) { - $this->getOutput()->writeln('Is not a json file: ' . $filename . ''); - $this->validateData(); - } - return $this; + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function (string $answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($this->getInput(), $this->getOutput(), $question); + $this->getInput()->setOption('data', $data); + $this->validateData(); + } + + public function bootstrap(): void { + $this->setSystem($this->getInput()->getOption('system')); + parent::bootstrap(); } public function import(): void { + $this->getOutput()->writeln('Starting import...'); + $this->bootstrap(); $this->getOutput()->writeln('Importing board...'); $this->importBoard(); $this->getOutput()->writeln('Assign users to board...'); diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 49a8bc4f4..a295f12a2 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -111,10 +111,10 @@ class BoardImportService { $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; - $this->setData(new \stdClass()); } public function import(): void { + $this->bootstrap(); try { $this->importBoard(); $this->importAcl(); @@ -129,12 +129,6 @@ class BoardImportService { } } - public function validate(): void { - $this->validateSystem(); - $this->validateConfig(); - $this->validateUsers(); - } - public function validateSystem(): void { if (!in_array($this->getSystem(), $this->getAllowedImportSystems())) { throw new NotFoundException('Invalid system'); @@ -327,7 +321,7 @@ class BoardImportService { $this->getImportSystem()->importParticipants(); } - final public function setData(\stdClass $data): void { + public function setData(\stdClass $data): void { $this->data = $data; } @@ -355,7 +349,7 @@ class BoardImportService { * @param string $configName config name * @return mixed */ - public function getConfig(string $configName = null) { + public function getConfig(string $configName) { if (!property_exists($this->config, $configName)) { return; } @@ -363,19 +357,10 @@ class BoardImportService { } /** - * @param mixed $config + * @param \stdClass $config * @return self */ public function setConfigInstance($config): self { - if (is_string($config)) { - if (!is_file($config)) { - throw new NotFoundException('Please inform a valid config json file'); - } - $config = json_decode(file_get_contents($config)); - if (!is_object($config)) { - throw new NotFoundException('Please inform a valid config json file'); - } - } $this->config = $config; return $this; } @@ -413,7 +398,13 @@ class BoardImportService { $this->setConfig('owner', $owner); } - public function validateUsers(): void { - $this->getImportSystem()->validateUsers(); + protected function validateData(): void { + } + + public function bootstrap(): void { + $this->validateSystem(); + $this->validateConfig(); + $this->validateData(); + $this->getImportSystem()->bootstrap(); } } diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloService.php index adf8b0abe..1598e20f2 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -66,6 +66,10 @@ class BoardImportTrelloService extends ABoardImportService { $this->l10n = $l10n; } + public function bootstrap(): void { + $this->validateUsers(); + } + public function validateUsers(): void { if (empty($this->getImportService()->getConfig('uidRelation'))) { return; diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index a24022370..9d0ccbd6b 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -76,6 +76,22 @@ class BoardImportServiceTest extends \Test\TestCase { $this->assignmentMapper, $this->commentsManager ); + + $this->boardImportService->setSystem('trello'); + + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $this->boardImportService->setData($data); + + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $this->boardImportService->setConfigInstance($configInstance); + + $owner = $this->createMock(IUser::class); + $owner + ->method('getUID') + ->willReturn('admin'); + $this->userManager + ->method('get') + ->willReturn($owner); } public function testImportSuccess() { @@ -91,25 +107,18 @@ class BoardImportServiceTest extends \Test\TestCase { } public function testImportBoard() { - $this->boardImportService->setSystem('trello'); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); - $this->boardImportService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); - $this->boardImportService->setConfigInstance($configInstance); - - $owner = $this->createMock(IUser::class); - $owner - ->method('getUID') - ->willReturn('owner'); - $this->userManager - ->method('get') - ->willReturn($owner); $this->boardImportService->validateOwner(); $actual = $this->boardImportService->importBoard(); $this->assertNull($actual); $board = $this->boardImportService->getBoard(); $this->assertEquals('Test Board Name', $board->getTitle()); - $this->assertEquals('owner', $board->getOwner()); + $this->assertEquals('admin', $board->getOwner()); $this->assertEquals('0800fd', $board->getColor()); } + + public function testImportAcl() { + $this->markTestIncomplete(); + $actual = $this->boardImportService->importAcl(); + $this->assertNull($actual); + } } From 19c609540b46a6087c117ad4d03a2140cc2cb035 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 19 Jul 2021 09:03:42 -0300 Subject: [PATCH 11/21] Default valute to board property fix command flow and refactor Fixes on getBoard tests Refactor Reduce psalm info Refactor to implement pattern Change order of methods to put all abstract first and all public first Signed-off-by: Vitor Mattos --- lib/Controller/BoardImportApiController.php | 4 +- lib/Service/ABoardImportService.php | 76 ++++- lib/Service/BoardImportCommandService.php | 11 +- lib/Service/BoardImportService.php | 119 +++---- lib/Service/BoardImportTrelloService.php | 320 +++++++++--------- tests/data/config-trello.json | 2 +- tests/unit/Service/BoardImportServiceTest.php | 119 +++++-- .../Service/BoardImportTrelloServiceTest.php | 46 ++- 8 files changed, 388 insertions(+), 309 deletions(-) diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php index b6c32183f..2d2545105 100644 --- a/lib/Controller/BoardImportApiController.php +++ b/lib/Controller/BoardImportApiController.php @@ -24,12 +24,12 @@ namespace OCA\Deck\Controller; use OCA\Deck\Service\BoardImportService; -use OCP\AppFramework\ApiController; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\IRequest; -class BoardImportApiController extends ApiController { +class BoardImportApiController extends OCSController { /** @var BoardImportService */ private $boardImportService; /** @var string */ diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 614c31225..6b942410e 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -24,14 +24,38 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Assignment; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; +use OCP\AppFramework\Db\Entity; +use OCP\Comments\IComment; abstract class ABoardImportService { /** @var BoardImportService */ private $boardImportService; + /** @var Stack[] */ + protected $stacks = []; + /** @var Label[] */ + protected $labels = []; + /** @var Card[] */ + protected $cards = []; + /** @var Acl[] */ + protected $acls = []; + /** @var IComment[][] */ + protected $comments = []; + /** @var Assignment[] */ + protected $assignments = []; + /** @var string[][] */ + protected $labelCardAssignments = []; + + /** + * Configure import service + * + * @return void + */ + abstract public function bootstrap(): void; abstract public function getBoard(): ?Board; @@ -50,25 +74,47 @@ abstract class ABoardImportService { */ abstract public function getCards(): array; - abstract public function updateStack(string $id, Stack $stack): void; + abstract public function getCardAssignments(): array; - abstract public function updateCard(string $id, Card $card): void; - - abstract public function importParticipants(): void; - - abstract public function importComments(): void; - - /** @return Label[] */ - abstract public function importLabels(): array; - - abstract public function assignCardsToLabels(): void; + abstract public function getCardLabelAssignment(): array; /** - * Configure import service - * - * @return void + * @return IComment[][]|array */ - abstract public function bootstrap(): void; + abstract public function getComments(): array; + + /** @return Label[] */ + abstract public function getLabels(): array; + + abstract public function validateUsers(): void; + + public function updateStack(string $id, Stack $stack): void { + $this->stacks[$id] = $stack; + } + + public function updateCard(string $id, Card $card): void { + $this->cards[$id] = $card; + } + + public function updateLabel(string $code, Label $label): void { + $this->labels[$code] = $label; + } + + public function updateAcl(string $code, Acl $acl): void { + $this->acls[$code] = $acl; + } + + public function updateComment(string $cardId, string $commentId, IComment $comment): void { + $this->comments[$cardId][$commentId] = $comment; + } + + public function updateCardAssignment(string $cardId, string $assignmentId, Entity $assignment): void { + $this->assignments[$cardId][$assignmentId] = $assignment; + } + + public function updateCardLabelsAssignment(string $cardId, string $assignmentId, string $assignment): void { + $this->labelCardAssignments[$cardId][$assignmentId] = $assignment; + } public function setImportService(BoardImportService $service): void { $this->boardImportService = $service; diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index bff49e5a0..1a3300cc1 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -114,10 +114,9 @@ class BoardImportCommandService extends BoardImportService { $this->getOutput()->writeln('Valid schema:'); $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); - $this->getInput()->setOption('config', null); + $this->getInput()->setOption('config', ''); } - parent::validateConfig(); - return; + $this->validateConfig(); } public function validateSystem(): void { @@ -186,9 +185,9 @@ class BoardImportCommandService extends BoardImportService { $this->importCards(); $this->getOutput()->writeln('Assign cards to labels...'); $this->assignCardsToLabels(); - $this->getOutput()->writeln('Iporting comments...'); + $this->getOutput()->writeln('Importing comments...'); $this->importComments(); - $this->getOutput()->writeln('Iporting participants...'); - $this->importParticipants(); + $this->getOutput()->writeln('Importing participants...'); + $this->importCardAssignments(); } } diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index a295f12a2..61e126177 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -25,22 +25,20 @@ namespace OCA\Deck\Service; use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; -use OC\Comments\Comment; use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\AclMapper; -use OCA\Deck\Db\Assignment; use OCA\Deck\Db\AssignmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; -use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; use OCP\AppFramework\Db\Entity; +use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException as CommentNotFoundException; use OCP\IDBConnection; @@ -87,7 +85,6 @@ class BoardImportService { private $data; /** * @var Board - * @psalm-suppress PropertyNotSetInConstructor */ private $board; @@ -111,6 +108,21 @@ class BoardImportService { $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; + $this->board = new Board(); + $this->disableCommentsEvents(); + } + + private function disableCommentsEvents(): void { + if (defined('PHPUNIT_RUN')) { + return; + } + $propertyEventHandlers = new \ReflectionProperty($this->commentsManager, 'eventHandlers'); + $propertyEventHandlers->setAccessible(true); + $propertyEventHandlers->setValue($this->commentsManager, []); + + $propertyEventHandlerClosures = new \ReflectionProperty($this->commentsManager, 'eventHandlerClosures'); + $propertyEventHandlerClosures->setAccessible(true); + $propertyEventHandlerClosures->setValue($this->commentsManager, []); } public function import(): void { @@ -123,7 +135,7 @@ class BoardImportService { $this->importCards(); $this->assignCardsToLabels(); $this->importComments(); - $this->importParticipants(); + $this->importCardAssignments(); } catch (\Throwable $th) { throw new BadRequestException($th->getMessage()); } @@ -171,15 +183,14 @@ class BoardImportService { } public function getImportSystem(): ABoardImportService { - $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; if (!$this->getSystem()) { throw new NotFoundException('System to import not found'); } if (!is_object($this->systemInstance)) { + $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; $this->systemInstance = \OC::$server->get($systemClass); $this->systemInstance->setImportService($this); } - return $this->systemInstance; } @@ -187,11 +198,6 @@ class BoardImportService { $this->systemInstance = $instance; } - public function insertAssignment(Assignment $assignment): self { - $this->assignmentMapper->insert($assignment); - return $this; - } - public function importBoard(): void { $board = $this->getImportSystem()->getBoard(); if ($board) { @@ -200,23 +206,29 @@ class BoardImportService { } } - public function getBoard(): Board { + public function getBoard(bool $reset = false): Board { + if ($reset) { + $this->board = new Board(); + } return $this->board; } - public function importAcl(): self { + public function importAcl(): void { $aclList = $this->getImportSystem()->getAclList(); - foreach ($aclList as $acl) { + foreach ($aclList as $code => $acl) { $this->aclMapper->insert($acl); + $this->getImportSystem()->updateAcl($code, $acl); } $this->getBoard()->setAcl($aclList); - return $this; } - public function importLabels(): array { - $labels = $this->getImportSystem()->importLabels(); + public function importLabels(): void { + $labels = $this->getImportSystem()->getLabels(); + foreach ($labels as $code => $label) { + $this->labelMapper->insert($label); + $this->getImportSystem()->updateLabel($code, $label); + } $this->getBoard()->setLabels($labels); - return $labels; } public function createLabel(string $title, string $color, int $boardId): Entity { @@ -227,26 +239,21 @@ class BoardImportService { return $this->labelMapper->insert($label); } - /** - * @return Stack[] - */ - public function importStacks(): array { + public function importStacks(): void { $stacks = $this->getImportSystem()->getStacks(); foreach ($stacks as $code => $stack) { $this->stackMapper->insert($stack); $this->getImportSystem()->updateStack($code, $stack); } $this->getBoard()->setStacks(array_values($stacks)); - return $stacks; } - public function importCards(): self { + public function importCards(): void { $cards = $this->getImportSystem()->getCards(); foreach ($cards as $code => $card) { $this->cardMapper->insert($card); $this->getImportSystem()->updateCard($code, $card); } - return $this; } /** @@ -263,21 +270,36 @@ class BoardImportService { } public function assignCardsToLabels(): void { - $this->getImportSystem()->assignCardsToLabels(); + $data = $this->getImportSystem()->getCardLabelAssignment(); + foreach ($data as $cardId => $assignemnt) { + foreach ($assignemnt as $assignmentId => $labelId) { + $this->assignCardToLabel( + $cardId, + $labelId + ); + $this->getImportSystem()->updateCardLabelsAssignment($cardId, $assignmentId, $labelId); + } + } } public function importComments(): void { - $this->getImportSystem()->importComments(); + $allComments = $this->getImportSystem()->getComments(); + foreach ($allComments as $cardId => $comments) { + foreach ($comments as $commentId => $comment) { + $this->insertComment($cardId, $comment); + $this->getImportSystem()->updateComment($cardId, $commentId, $comment); + } + } } - public function insertComment(string $cardId, Comment $comment): void { + private function insertComment(string $cardId, IComment $comment): void { $comment->setObject('deckCard', $cardId); $comment->setVerb('comment'); // Check if parent is a comment on the same card if ($comment->getParentId() !== '0') { try { - $comment = $this->commentsManager->get($comment->getParentId()); - if ($comment->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $comment->getObjectId() !== $cardId) { + $parent = $this->commentsManager->get($comment->getParentId()); + if ($parent->getObjectType() !== Application::COMMENT_ENTITY_TYPE || $parent->getObjectId() !== $cardId) { throw new CommentNotFoundException(); } } catch (CommentNotFoundException $e) { @@ -286,30 +308,7 @@ class BoardImportService { } try { - $qb = $this->dbConn->getQueryBuilder(); - - $values = [ - 'parent_id' => $qb->createNamedParameter($comment->getParentId()), - 'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()), - 'children_count' => $qb->createNamedParameter($comment->getChildrenCount()), - 'actor_type' => $qb->createNamedParameter($comment->getActorType()), - 'actor_id' => $qb->createNamedParameter($comment->getActorId()), - 'message' => $qb->createNamedParameter($comment->getMessage()), - 'verb' => $qb->createNamedParameter($comment->getVerb()), - 'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'), - 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'), - 'object_type' => $qb->createNamedParameter($comment->getObjectType()), - 'object_id' => $qb->createNamedParameter($comment->getObjectId()), - 'reference_id' => $qb->createNamedParameter($comment->getReferenceId()) - ]; - - $affectedRows = $qb->insert('comments') - ->values($values) - ->execute(); - - if ($affectedRows > 0) { - $comment->setId((string)$qb->getLastInsertId()); - } + $this->commentsManager->save($comment); } catch (\InvalidArgumentException $e) { throw new BadRequestException('Invalid input values'); } catch (CommentNotFoundException $e) { @@ -317,8 +316,14 @@ class BoardImportService { } } - public function importParticipants(): void { - $this->getImportSystem()->importParticipants(); + public function importCardAssignments(): void { + $allAssignments = $this->getImportSystem()->getCardAssignments(); + foreach ($allAssignments as $cardId => $assignments) { + foreach ($assignments as $assignmentId => $assignment) { + $this->assignmentMapper->insert($assignment); + $this->getImportSystem()->updateCardAssignment($cardId, $assignmentId, $assignment); + } + } } public function setData(\stdClass $data): void { diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloService.php index 1598e20f2..3068ad66f 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloService.php @@ -31,7 +31,6 @@ use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; -use OCP\AppFramework\Db\Entity; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -41,20 +40,6 @@ class BoardImportTrelloService extends ABoardImportService { private $userManager; /** @var IL10N */ private $l10n; - /** - * Array of stacks - * - * @var Stack[] - */ - private $stacks = []; - /** - * Array of Labels - * - * @var Label[]|Entity[] - */ - private $labels = []; - /** @var Card[] */ - private $cards = []; /** @var IUser[] */ private $members = []; @@ -94,46 +79,115 @@ class BoardImportTrelloService extends ABoardImportService { } } - /** - * @return Acl[] - */ - public function getAclList(): array { - $return = []; - foreach ($this->members as $member) { - if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { - continue; + public function getCardAssignments(): array { + $assignments = []; + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->idMembers as $idMember) { + if (empty($this->members[$idMember])) { + continue; + } + $assignment = new Assignment(); + $assignment->setCardId($this->cards[$trelloCard->id]->getId()); + $assignment->setParticipant($this->members[$idMember]->getUID()); + $assignment->setType(Assignment::TYPE_USER); + $assignments[$trelloCard->id][] = $assignment; } - $acl = new Acl(); - $acl->setBoardId($this->getImportService()->getBoard()->getId()); - $acl->setType(Acl::PERMISSION_TYPE_USER); - $acl->setParticipant($member->getUID()); - $acl->setPermissionEdit(false); - $acl->setPermissionShare(false); - $acl->setPermissionManage(false); - $return[] = $acl; + } + return $assignments; + } + + public function getComments(): array { + $comments = []; + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + $values = array_filter( + $this->getImportService()->getData()->actions, + function (\stdClass $a) use ($trelloCard) { + return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + } + ); + $keys = array_map(function (\stdClass $c): string { + return $c->id; + }, $values); + $trelloComments = array_combine($keys, $values); + foreach ($trelloComments as $commentId => $trelloComment) { + $comment = new Comment(); + if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + } else { + $actor = $this->getImportService()->getConfig('owner')->getUID(); + } + $comment + ->setActor('users', $actor) + ->setMessage($this->replaceUsernames($trelloComment->data->text), 0) + ->setCreationDateTime( + \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) + ); + $cardId = $this->cards[$trelloCard->id]->getId(); + $comments[$cardId][$commentId] = $comment; + } + } + return $comments; + } + + public function getCardLabelAssignment(): array { + $cardsLabels = []; + foreach ($this->getImportService()->getData()->cards as $trelloCard) { + foreach ($trelloCard->labels as $label) { + $cardId = $this->cards[$trelloCard->id]->getId(); + $labelId = $this->labels[$label->id]->getId(); + $cardsLabels[$cardId][] = $labelId; + } + } + return $cardsLabels; + } + + public function getBoard(): Board { + $board = $this->getImportService()->getBoard(); + if (empty($this->getImportService()->getData()->name)) { + throw new BadRequestException('Invalid name of board'); + } + $board->setTitle($this->getImportService()->getData()->name); + $board->setOwner($this->getImportService()->getConfig('owner')->getUID()); + $board->setColor($this->getImportService()->getConfig('color')); + return $board; + } + + /** + * @return Label[] + */ + public function getLabels(): array { + foreach ($this->getImportService()->getData()->labels as $trelloLabel) { + $label = new Label(); + if (empty($trelloLabel->name)) { + $label->setTitle('Unnamed ' . $trelloLabel->color . ' label'); + } else { + $label->setTitle($trelloLabel->name); + } + $label->setColor($this->translateColor($trelloLabel->color)); + $label->setBoardId($this->getImportService()->getBoard()->getId()); + $this->labels[$trelloLabel->id] = $label; + } + return $this->labels; + } + + /** + * @return Stack[] + */ + public function getStacks(): array { + $return = []; + foreach ($this->getImportService()->getData()->lists as $order => $list) { + $stack = new Stack(); + if ($list->closed) { + $stack->setDeletedAt(time()); + } + $stack->setTitle($list->name); + $stack->setBoardId($this->getImportService()->getBoard()->getId()); + $stack->setOrder($order + 1); + $return[$list->id] = $stack; } return $return; } - private function checklistItem(\stdClass $item): string { - if (($item->state == 'incomplete')) { - $string_start = '- [ ]'; - } else { - $string_start = '- [x]'; - } - $check_item_string = $string_start . ' ' . $item->name . "\n"; - return $check_item_string; - } - - private function formulateChecklistText(\stdClass $checklist): string { - $checklist_string = "\n\n## {$checklist->name}\n"; - foreach ($checklist->checkItems as $item) { - $checklist_item_string = $this->checklistItem($item); - $checklist_string = $checklist_string . "\n" . $checklist_item_string; - } - return $checklist_string; - } - /** * @return Card[] */ @@ -144,6 +198,7 @@ class BoardImportTrelloService extends ABoardImportService { } $this->getImportService()->getData()->checklists = $checklists; + $cards = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { $card = new Card(); $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); @@ -172,112 +227,32 @@ class BoardImportTrelloService extends ABoardImportService { ->format('Y-m-d H:i:s'); $card->setDuedate($duedate); } - $this->cards[$trelloCard->id] = $card; - } - return $this->cards; - } - - public function updateCard(string $id, Card $card): void { - $this->cards[$id] = $card; - } - - private function appendAttachmentsToDescription(\stdClass $trelloCard): void { - if (empty($trelloCard->attachments)) { - return; - } - $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; - $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; - $trelloCard->desc .= "|---|---\n"; - foreach ($trelloCard->attachments as $attachment) { - $name = $attachment->name === $attachment->url ? null : $attachment->name; - $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; - } - } - - public function importParticipants(): void { - foreach ($this->getImportService()->getData()->cards as $trelloCard) { - foreach ($trelloCard->idMembers as $idMember) { - if (empty($this->members[$idMember])) { - continue; - } - $assignment = new Assignment(); - $assignment->setCardId($this->cards[$trelloCard->id]->getId()); - $assignment->setParticipant($this->members[$idMember]->getUID()); - $assignment->setType(Assignment::TYPE_USER); - $this->getImportService()->insertAssignment($assignment); - } - } - } - - public function importComments(): void { - foreach ($this->getImportService()->getData()->cards as $trelloCard) { - $comments = array_filter( - $this->getImportService()->getData()->actions, - function (\stdClass $a) use ($trelloCard) { - return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; - } - ); - foreach ($comments as $trelloComment) { - if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { - $actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); - } else { - $actor = $this->getImportService()->getConfig('owner')->getUID(); - } - $comment = new Comment(); - $comment - ->setActor('users', $actor) - ->setMessage($this->replaceUsernames($trelloComment->data->text), 0) - ->setCreationDateTime( - \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) - ); - $this->getImportService()->insertComment( - (string) $this->cards[$trelloCard->id]->getId(), - $comment - ); - } - } - } - - private function replaceUsernames(string $text): string { - foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { - $text = str_replace($trello, $nextcloud->getUID(), $text); - } - return $text; - } - - public function assignCardsToLabels(): void { - foreach ($this->getImportService()->getData()->cards as $trelloCard) { - foreach ($trelloCard->labels as $label) { - $this->getImportService()->assignCardToLabel( - $this->cards[$trelloCard->id]->getId(), - $this->labels[$label->id]->getId() - ); - } + $cards[$trelloCard->id] = $card; } + return $cards; } /** - * @return Stack[] + * @return Acl[] */ - public function getStacks(): array { + public function getAclList(): array { $return = []; - foreach ($this->getImportService()->getData()->lists as $order => $list) { - $stack = new Stack(); - if ($list->closed) { - $stack->setDeletedAt(time()); + foreach ($this->members as $member) { + if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { + continue; } - $stack->setTitle($list->name); - $stack->setBoardId($this->getImportService()->getBoard()->getId()); - $stack->setOrder($order + 1); - $return[$list->id] = $stack; + $acl = new Acl(); + $acl->setBoardId($this->getImportService()->getBoard()->getId()); + $acl->setType(Acl::PERMISSION_TYPE_USER); + $acl->setParticipant($member->getUID()); + $acl->setPermissionEdit(false); + $acl->setPermissionShare(false); + $acl->setPermissionManage(false); + $return[] = $acl; } return $return; } - public function updateStack(string $id, Stack $stack): void { - $this->stacks[$id] = $stack; - } - private function translateColor(string $color): string { switch ($color) { case 'red': @@ -305,31 +280,42 @@ class BoardImportTrelloService extends ABoardImportService { } } - public function getBoard(): Board { - $board = new Board(); - if (empty($this->getImportService()->getData()->name)) { - throw new BadRequestException('Invalid name of board'); + private function replaceUsernames(string $text): string { + foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { + $text = str_replace($trello, $nextcloud->getUID(), $text); } - $board->setTitle($this->getImportService()->getData()->name); - $board->setOwner($this->getImportService()->getConfig('owner')->getUID()); - $board->setColor($this->getImportService()->getConfig('color')); - return $board; + return $text; } - public function importLabels(): array { - foreach ($this->getImportService()->getData()->labels as $label) { - if (empty($label->name)) { - $labelTitle = 'Unnamed ' . $label->color . ' label'; - } else { - $labelTitle = $label->name; - } - $newLabel = $this->getImportService()->createLabel( - $labelTitle, - $this->translateColor($label->color), - $this->getImportService()->getBoard()->getId() - ); - $this->labels[$label->id] = $newLabel; + private function checklistItem(\stdClass $item): string { + if (($item->state == 'incomplete')) { + $string_start = '- [ ]'; + } else { + $string_start = '- [x]'; + } + $check_item_string = $string_start . ' ' . $item->name . "\n"; + return $check_item_string; + } + + private function formulateChecklistText(\stdClass $checklist): string { + $checklist_string = "\n\n## {$checklist->name}\n"; + foreach ($checklist->checkItems as $item) { + $checklist_item_string = $this->checklistItem($item); + $checklist_string = $checklist_string . "\n" . $checklist_item_string; + } + return $checklist_string; + } + + private function appendAttachmentsToDescription(\stdClass $trelloCard): void { + if (empty($trelloCard->attachments)) { + return; + } + $trelloCard->desc .= "\n\n## {$this->l10n->t('Attachments')}\n"; + $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; + $trelloCard->desc .= "|---|---\n"; + foreach ($trelloCard->attachments as $attachment) { + $name = $attachment->name === $attachment->url ? null : $attachment->name; + $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; } - return $this->labels; } } diff --git a/tests/data/config-trello.json b/tests/data/config-trello.json index 544118212..48a27f54e 100644 --- a/tests/data/config-trello.json +++ b/tests/data/config-trello.json @@ -2,6 +2,6 @@ "owner": "admin", "color": "0800fd", "uidRelation": { - "johndoe": "admin" + "johndoe": "johndoe" } } \ No newline at end of file diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index 9d0ccbd6b..e0baab7c3 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -22,38 +22,46 @@ */ namespace OCA\Deck\Service; +use OC\Comments\Comment; +use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Assignment; use OCA\Deck\Db\AssignmentMapper; -use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; +use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; use OCP\Comments\ICommentsManager; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; class BoardImportServiceTest extends \Test\TestCase { - /** @var IDBConnection */ + /** @var IDBConnection|MockObject */ protected $dbConn; - /** @var IUserManager */ + /** @var IUserManager|MockObject */ private $userManager; - /** @var BoardMapper */ + /** @var BoardMapper|MockObject */ private $boardMapper; - /** @var AclMapper */ + /** @var AclMapper|MockObject */ private $aclMapper; - /** @var LabelMapper */ + /** @var LabelMapper|MockObject */ private $labelMapper; - /** @var StackMapper */ + /** @var StackMapper|MockObject */ private $stackMapper; - /** @var CardMapper */ + /** @var CardMapper|MockObject */ private $cardMapper; - /** @var AssignmentMapper */ + /** @var AssignmentMapper|MockObject */ private $assignmentMapper; - /** @var ICommentsManager */ + /** @var ICommentsManager|MockObject */ private $commentsManager; - /** @var BoardImportService */ + /** @var BoardImportTrelloService|MockObject */ + private $importTrelloService; + /** @var BoardImportService|MockObject */ private $boardImportService; public function setUp(): void { $this->dbConn = $this->createMock(IDBConnection::class); @@ -85,40 +93,83 @@ class BoardImportServiceTest extends \Test\TestCase { $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); $this->boardImportService->setConfigInstance($configInstance); + $this->importTrelloService = $this->createMock(BoardImportTrelloService::class); + $this->boardImportService->setImportSystem($this->importTrelloService); + $owner = $this->createMock(IUser::class); $owner ->method('getUID') ->willReturn('admin'); + + $johndoe = $this->createMock(IUser::class); + $johndoe + ->method('getUID') + ->willReturn('johndoe'); $this->userManager ->method('get') - ->willReturn($owner); + ->withConsecutive( + ['admin'], + ['johndoe'] + ) + ->willReturnonConsecutiveCalls( + $owner, + $johndoe + ); } public function testImportSuccess() { - $importService = $this->createMock(ABoardImportService::class); - $board = new Board(); - $importService - ->method('getBoard') - ->willReturn($board); - $this->boardImportService->setSystem('trello'); - $this->boardImportService->setImportSystem($importService); + $this->boardMapper + ->expects($this->once()) + ->method('insert'); + + $this->importTrelloService + ->method('getAclList') + ->willReturn([new Acl()]); + $this->aclMapper + ->expects($this->once()) + ->method('insert'); + + $this->importTrelloService + ->method('getLabels') + ->willReturn([new Label()]); + $this->labelMapper + ->expects($this->once()) + ->method('insert'); + + $this->importTrelloService + ->method('getStacks') + ->willReturn([new Stack()]); + $this->stackMapper + ->expects($this->once()) + ->method('insert'); + + $this->importTrelloService + ->method('getCards') + ->willReturn([new Card()]); + $this->cardMapper + ->expects($this->any()) + ->method('insert'); + + $this->importTrelloService + ->method('getComments') + ->willReturn([ + 'fakecardid' => [new Comment()] + ]); + $this->commentsManager + ->expects($this->once()) + ->method('save'); + + $this->importTrelloService + ->method('getCardAssignments') + ->willReturn([ + 'fakecardid' => [new Assignment()] + ]); + $this->assignmentMapper + ->expects($this->once()) + ->method('insert'); + $actual = $this->boardImportService->import(); - $this->assertNull($actual); - } - public function testImportBoard() { - $this->boardImportService->validateOwner(); - $actual = $this->boardImportService->importBoard(); - $this->assertNull($actual); - $board = $this->boardImportService->getBoard(); - $this->assertEquals('Test Board Name', $board->getTitle()); - $this->assertEquals('admin', $board->getOwner()); - $this->assertEquals('0800fd', $board->getColor()); - } - - public function testImportAcl() { - $this->markTestIncomplete(); - $actual = $this->boardImportService->importAcl(); $this->assertNull($actual); } } diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/BoardImportTrelloServiceTest.php index dbfbcd7eb..734347008 100644 --- a/tests/unit/Service/BoardImportTrelloServiceTest.php +++ b/tests/unit/Service/BoardImportTrelloServiceTest.php @@ -22,7 +22,6 @@ */ namespace OCA\Deck\Service; -use OCA\Deck\Db\Board; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; @@ -114,39 +113,32 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { $this->assertNull($actual); } - public function testGetBoardWithSuccess() { + public function testGetBoardWithNoName() { + $this->expectErrorMessage('Invalid name of board'); $importService = $this->createMock(BoardImportService::class); + $this->service->setImportService($importService); + $this->service->getBoard(); + } + + public function testGetBoardWithSuccess() { + $importService = \OC::$server->get(BoardImportService::class); + + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $importService->setData($data); + + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $importService->setConfigInstance($configInstance); + $owner = $this->createMock(IUser::class); $owner ->method('getUID') ->willReturn('owner'); - $importService - ->method('getConfig') - ->withConsecutive( - ['owner'], - ['color'] - )->willReturnonConsecutiveCalls( - $owner, - '000000' - ); - $importService - ->method('getData') - ->willReturn(json_decode('{"name": "test"}')); + $importService->setConfig('owner', $owner); + $this->service->setImportService($importService); $actual = $this->service->getBoard(); - $this->assertInstanceOf(Board::class, $actual); - $this->assertEquals('test', $actual->getTitle()); + $this->assertEquals('Test Board Name', $actual->getTitle()); $this->assertEquals('owner', $actual->getOwner()); - $this->assertEquals('000000', $actual->getColor()); - } - - public function testGetBoardWithInvalidName() { - $this->expectErrorMessage('Invalid name of board'); - $importService = $this->createMock(BoardImportService::class); - $importService - ->method('getData') - ->willReturn(new \stdClass); - $this->service->setImportService($importService); - $this->service->getBoard(); + $this->assertEquals('0800fd', $actual->getColor()); } } From c7a37ea425c130ec4ad030f0c24d8f7abf402801 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 23 Jul 2021 14:17:20 -0300 Subject: [PATCH 12/21] Increase documentation Signed-off-by: Vitor Mattos --- docs/implement-import.md | 4 + docs/import-class-diagram.md | 7 ++ docs/resources/BoardImport.svg | 178 ++++++++++++++++++++++++++++++++ docs/resources/BoardImport.yuml | 17 +++ mkdocs.yml | 4 +- 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 docs/implement-import.md create mode 100644 docs/import-class-diagram.md create mode 100644 docs/resources/BoardImport.svg create mode 100644 docs/resources/BoardImport.yuml diff --git a/docs/implement-import.md b/docs/implement-import.md new file mode 100644 index 000000000..cca83357d --- /dev/null +++ b/docs/implement-import.md @@ -0,0 +1,4 @@ +## Implement import + +* Create a new class `lib/service/BoardImportService.php` where `` is the name of the source system. +* Use the `lib/service/BoardImportTrelloService.php` class as inspiration \ No newline at end of file diff --git a/docs/import-class-diagram.md b/docs/import-class-diagram.md new file mode 100644 index 000000000..cbd5b67ca --- /dev/null +++ b/docs/import-class-diagram.md @@ -0,0 +1,7 @@ +## Import class diagram + +Importing boards to the Deck implements the class diagram below. + +> **NOTE**: When making any changes to the structure of the classes or implementing import from other sources, edit the `BoardImport.yuml` file + +![Screenshot](resources/BoardImport.svg) \ No newline at end of file diff --git a/docs/resources/BoardImport.svg b/docs/resources/BoardImport.svg new file mode 100644 index 000000000..700538bdb --- /dev/null +++ b/docs/resources/BoardImport.svg @@ -0,0 +1,178 @@ + + + + + + +G + + + +A0 + + + +Classes used on +board import. +Methods just to +illustrate. + + + +A1 + +ApiController + + + +A2 + +BoardImportApiController + ++import() ++getAllowedSystems() ++getConfigSchema() + + + +A1->A2 + + + + + +A3 + +BoardImportService + ++import() ++bootstrap() ++validateSystem() +#validateConfig() +#validateData() + + + +A2->A3 + + +uses + + + +A7 + +BoardImportTrelloService + + + +A3->A7 + + +uses + + + +A8 + + + +validateSystem is +public because is +used on Api. + + + +A3->A8 + + + + +A4 + +Command + + + +A5 + +BoardImport + ++boardImportCommandService + +#configure() +#execute(input,output) + + + +A4->A5 + + + + + +A6 + +BoardImportCommandService + ++bootstrap() ++import() ++validateSystem() +#validateConfig() +#validateData() + + + +A5->A6 + + +uses + + + +A6->A3 + + + + + +A7->A3 + + +uses + + + +A9 + + + +To create an import +to another system, +create another class +similar to this. + + + +A7->A9 + + + + +A10 + +<<abstract>> +ABoardImportService + + + +A7->A10 + + +implements + + + diff --git a/docs/resources/BoardImport.yuml b/docs/resources/BoardImport.yuml new file mode 100644 index 000000000..69aa222da --- /dev/null +++ b/docs/resources/BoardImport.yuml @@ -0,0 +1,17 @@ +// Created using [yUML](https://github.com/jaime-olivares/vscode-yuml) + +// {type:class} +// {direction:topDown} +// {generate:true} + +[note: Classes used on board import. Methods just to illustrate. {bg:cornsilk}] +[ApiController]<-[BoardImportApiController|+import();+getAllowedSystems();+getConfigSchema()] +[BoardImportApiController]uses-.->[BoardImportService|+import();+bootstrap();+validateSystem();#validateConfig();#validateData();] +[Command]<-[BoardImport|+boardImportCommandService|#configure();#execute(input,output)] +[BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()] +[BoardImportCommandService]->[BoardImportService] +[BoardImportService]uses-.->[BoardImportTrelloService] +[BoardImportTrelloService]uses-.->[BoardImportService] +[BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}] +[BoardImportTrelloService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] +[BoardImportTrelloService]implements-.-^[<> ABoardImportService] \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c80c417be..3e8defc2e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,4 +11,6 @@ pages: - Nextcloud API: API-Nextcloud.md - Developer documentation: - Data structure: structure.md - + - Import documentation: + - Implement import: implement-import.md + - Class diagram: import-class-diagram.md From 202ea30090413466c30d7a82d112088f979ca182 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sat, 24 Jul 2021 20:26:34 -0300 Subject: [PATCH 13/21] Start implementing Trello API service Implement name of system to import Implement need validate data Fix allowed system list Start implementing Trello API service Signed-off-by: Vitor Mattos --- lib/Command/BoardImport.php | 7 +- lib/Service/ABoardImportService.php | 7 ++ lib/Service/BoardImportCommandService.php | 13 ++- lib/Service/BoardImportService.php | 26 ++++- lib/Service/BoardImportTrelloApiService.php | 99 +++++++++++++++++++ ...e.php => BoardImportTrelloJsonService.php} | 4 +- .../fixtures/config-trelloApi-schema.json | 44 +++++++++ ...ema.json => config-trelloJson-schema.json} | 0 ...fig-trello.json => config-trelloJson.json} | 0 ...{data-trello.json => data-trelloJson.json} | 0 tests/unit/Command/BoardImportTest.php | 2 +- tests/unit/Service/BoardImportServiceTest.php | 26 ++--- .../Service/BoardImportTrelloServiceTest.php | 16 +-- .../BoardImportApiControllerTest.php | 13 ++- 14 files changed, 219 insertions(+), 38 deletions(-) create mode 100644 lib/Service/BoardImportTrelloApiService.php rename lib/Service/{BoardImportTrelloService.php => BoardImportTrelloJsonService.php} (98%) create mode 100644 lib/Service/fixtures/config-trelloApi-schema.json rename lib/Service/fixtures/{config-trello-schema.json => config-trelloJson-schema.json} (100%) rename tests/data/{config-trello.json => config-trelloJson.json} (100%) rename tests/data/{data-trello.json => data-trelloJson.json} (100%) diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 2c825ae9e..0674b128a 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -45,6 +45,7 @@ class BoardImport extends Command { */ protected function configure() { $allowedSystems = $this->boardImportCommandService->getAllowedImportSystems(); + $names = array_column($allowedSystems, 'name'); $this ->setName('deck:import') ->setDescription('Import data') @@ -52,8 +53,8 @@ class BoardImport extends Command { 'system', null, InputOption::VALUE_REQUIRED, - 'Source system for import. Available options: ' . implode(', ', $allowedSystems) . '.', - 'trello' + 'Source system for import. Available options: ' . implode(', ', $names) . '.', + null ) ->addOption( 'config', @@ -65,7 +66,7 @@ class BoardImport extends Command { ->addOption( 'data', null, - InputOption::VALUE_REQUIRED, + InputOption::VALUE_OPTIONAL, 'Data file to import.', 'data.json' ) diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 6b942410e..746febbc9 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -33,8 +33,11 @@ use OCP\AppFramework\Db\Entity; use OCP\Comments\IComment; abstract class ABoardImportService { + /** @var string */ + public static $name = ''; /** @var BoardImportService */ private $boardImportService; + protected $needValidateData = true; /** @var Stack[] */ protected $stacks = []; /** @var Label[] */ @@ -123,4 +126,8 @@ abstract class ABoardImportService { public function getImportService(): BoardImportService { return $this->boardImportService; } + + public function needValidateData(): bool { + return $this->needValidateData; + } } diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index 1a3300cc1..a9ca01a74 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -126,19 +126,24 @@ class BoardImportCommandService extends BoardImportService { } catch (\Throwable $th) { } $helper = $this->getCommand()->getHelper('question'); + $allowedSystems = $this->getAllowedImportSystems(); + $names = array_column($allowedSystems, 'name'); $question = new ChoiceQuestion( 'Please inform a source system', - $this->getAllowedImportSystems(), + $names, 0 ); $question->setErrorMessage('System %s is invalid.'); - $system = $helper->ask($this->getInput(), $this->getOutput(), $question); - $this->getInput()->setOption('system', $system); - $this->setSystem($system); + $selectedName = $helper->ask($this->getInput(), $this->getOutput(), $question); + $className = $allowedSystems[array_flip($names)[$selectedName]]['internalName']; + $this->setSystem($className); return; } protected function validateData(): void { + if (!$this->getImportSystem()->needValidateData()) { + return; + } $data = $this->getInput()->getOption('data'); if (is_string($data)) { $data = json_decode(file_get_contents($data)); diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 61e126177..3a35a57aa 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -67,7 +67,7 @@ class BoardImportService { private $system = ''; /** @var null|ABoardImportService */ private $systemInstance; - /** @var string[] */ + /** @var array */ private $allowedSystems = []; /** * Data object created from config JSON @@ -142,7 +142,9 @@ class BoardImportService { } public function validateSystem(): void { - if (!in_array($this->getSystem(), $this->getAllowedImportSystems())) { + $allowedSystems = $this->getAllowedImportSystems(); + $allowedSystems = array_column($allowedSystems, 'internalName'); + if (!in_array($this->getSystem(), $allowedSystems)) { throw new NotFoundException('Invalid system'); } } @@ -173,9 +175,23 @@ class BoardImportService { } return true; }); - $allowedSystems = array_map(function ($name) { - preg_match('/\/BoardImport(?\w+)Service\.php$/', $name, $matches); - return lcfirst($matches['system']); + $allowedSystems = array_map(function ($filename) { + preg_match('/\/(?BoardImport(?\w+)Service)\.php$/', $filename, $matches); + $className = 'OCA\Deck\Service\\'.$matches['class']; + if (!class_exists($className)) { + /** @psalm-suppress UnresolvableInclude */ + require_once $name; + } + /** @psalm-suppress InvalidPropertyFetch */ + $name = $className::$name; + if (empty($name)) { + $name = lcfirst($matches['system']); + } + return [ + 'name' => $name, + 'class' => $className, + 'internalName' => lcfirst($matches['system']) + ]; }, $allowedSystems); $this->allowedSystems = array_values($allowedSystems); } diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php new file mode 100644 index 000000000..5a8c54117 --- /dev/null +++ b/lib/Service/BoardImportTrelloApiService.php @@ -0,0 +1,99 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Service; + +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\RequestException; +use OCP\AppFramework\Http; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IUserManager; + +class BoardImportTrelloApiService extends BoardImportTrelloJsonService { + /** @var string */ + public static $name = 'Trello API'; + protected $needValidateData = false; + /** @var IClient */ + private $httpClient; + /** @var ILogger */ + protected $logger; + /** @var string */ + private $baseApiUrl = 'https://api.trello.com/1'; + + + public function __construct( + IUserManager $userManager, + IL10N $l10n, + ILogger $logger, + IClientService $httpClientService + ) { + parent::__construct($userManager, $l10n); + $this->logger = $logger; + $this->httpClient = $httpClientService->newClient(); + } + + public function bootstrap(): void { + $this->getBoards(); + parent::bootstrap(); + } + + private function getBoards() { + $boards = $this->doRequest('/members/me/boards'); + } + + private function doRequest($path, $queryString = []) { + try { + $target = $this->baseApiUrl . $path; + $result = $this->httpClient + ->get($target, $this->getQueryString($queryString)) + ->getBody(); + $data = json_decode($result); + } catch (ClientException $e) { + $status = $e->getCode(); + if ($status === Http::STATUS_FORBIDDEN) { + $this->logger->info($target . ' refused.', ['app' => 'deck']); + } else { + $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'deck']); + } + } catch (RequestException $e) { + $this->logger->logException($e, [ + 'message' => 'Could not connect to ' . $target, + 'level' => ILogger::INFO, + 'app' => 'deck', + ]); + } catch (\Throwable $e) { + $this->logger->logException($e, ['app' => 'deck']); + } + return $data; + } + + private function getQueryString($params = []): array { + $apiSettings = $this->getImportService()->getConfig('api'); + $params['key'] = $apiSettings->key; + $params['value'] = $apiSettings->token; + return $params; + } +} diff --git a/lib/Service/BoardImportTrelloService.php b/lib/Service/BoardImportTrelloJsonService.php similarity index 98% rename from lib/Service/BoardImportTrelloService.php rename to lib/Service/BoardImportTrelloJsonService.php index 3068ad66f..9bd7f6ff4 100644 --- a/lib/Service/BoardImportTrelloService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -35,7 +35,9 @@ use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -class BoardImportTrelloService extends ABoardImportService { +class BoardImportTrelloJsonService extends ABoardImportService { + /** @var string */ + public static $name = 'Trello JSON'; /** @var IUserManager */ private $userManager; /** @var IL10N */ diff --git a/lib/Service/fixtures/config-trelloApi-schema.json b/lib/Service/fixtures/config-trelloApi-schema.json new file mode 100644 index 000000000..baef76ce5 --- /dev/null +++ b/lib/Service/fixtures/config-trelloApi-schema.json @@ -0,0 +1,44 @@ +{ + "type": "object", + "properties": { + "api": { + "type": "object", + "properties": { + "key": { + "type": "string", + "pattern": "^\\w{32}$" + }, + "token": { + "type": "string", + "pattern": "^\\w{1,}$" + } + } + }, + "boards": { + "type": "array", + "items": { + "type": "string", + "pattern": "^\\w{1,}$" + } + }, + "uidRelation": { + "type": "object", + "comment": "Relationship between Trello and Nextcloud usernames", + "example": { + "johndoe": "admin" + } + }, + "owner": { + "type": "string", + "required": true, + "comment": "Nextcloud owner username" + }, + "color": { + "type": "string", + "required": true, + "pattern": "^[0-9a-fA-F]{6}$", + "comment": "Default color for the board. If you don't inform, the default color will be used.", + "default": "0800fd" + } + } +} \ No newline at end of file diff --git a/lib/Service/fixtures/config-trello-schema.json b/lib/Service/fixtures/config-trelloJson-schema.json similarity index 100% rename from lib/Service/fixtures/config-trello-schema.json rename to lib/Service/fixtures/config-trelloJson-schema.json diff --git a/tests/data/config-trello.json b/tests/data/config-trelloJson.json similarity index 100% rename from tests/data/config-trello.json rename to tests/data/config-trelloJson.json diff --git a/tests/data/data-trello.json b/tests/data/data-trelloJson.json similarity index 100% rename from tests/data/data-trello.json rename to tests/data/data-trelloJson.json diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index e2cdbbfbc..8c0c23119 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -58,7 +58,7 @@ class BoardImportTest extends \Test\TestCase { ['config'] ) ->will($this->returnValueMap([ - ['system', 'trello'], + ['system', 'trelloJson'], ['config', null] ])); diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index e0baab7c3..49f2afcd9 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -59,8 +59,8 @@ class BoardImportServiceTest extends \Test\TestCase { private $assignmentMapper; /** @var ICommentsManager|MockObject */ private $commentsManager; - /** @var BoardImportTrelloService|MockObject */ - private $importTrelloService; + /** @var BoardImportTrelloJsonService|MockObject */ + private $importTrelloJsonService; /** @var BoardImportService|MockObject */ private $boardImportService; public function setUp(): void { @@ -85,16 +85,16 @@ class BoardImportServiceTest extends \Test\TestCase { $this->commentsManager ); - $this->boardImportService->setSystem('trello'); + $this->boardImportService->setSystem('trelloJson'); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trelloJson.json')); $this->boardImportService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trelloJson.json')); $this->boardImportService->setConfigInstance($configInstance); - $this->importTrelloService = $this->createMock(BoardImportTrelloService::class); - $this->boardImportService->setImportSystem($this->importTrelloService); + $this->importTrelloJsonService = $this->createMock(BoardImportTrelloJsonService::class); + $this->boardImportService->setImportSystem($this->importTrelloJsonService); $owner = $this->createMock(IUser::class); $owner @@ -122,35 +122,35 @@ class BoardImportServiceTest extends \Test\TestCase { ->expects($this->once()) ->method('insert'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getAclList') ->willReturn([new Acl()]); $this->aclMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getLabels') ->willReturn([new Label()]); $this->labelMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getStacks') ->willReturn([new Stack()]); $this->stackMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getCards') ->willReturn([new Card()]); $this->cardMapper ->expects($this->any()) ->method('insert'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getComments') ->willReturn([ 'fakecardid' => [new Comment()] @@ -159,7 +159,7 @@ class BoardImportServiceTest extends \Test\TestCase { ->expects($this->once()) ->method('save'); - $this->importTrelloService + $this->importTrelloJsonService ->method('getCardAssignments') ->willReturn([ 'fakecardid' => [new Assignment()] diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/BoardImportTrelloServiceTest.php index 734347008..99c8f8c2d 100644 --- a/tests/unit/Service/BoardImportTrelloServiceTest.php +++ b/tests/unit/Service/BoardImportTrelloServiceTest.php @@ -26,8 +26,8 @@ use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -class BoardImportTrelloServiceTest extends \Test\TestCase { - /** @var BoardImportTrelloService */ +class BoardImportTrelloJsonServiceTest extends \Test\TestCase { + /** @var BoardImportTrelloJsonService */ private $service; /** @var IUserManager */ private $userManager; @@ -36,7 +36,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { public function setUp(): void { $this->userManager = $this->createMock(IUserManager::class); $this->l10n = $this->createMock(IL10N::class); - $this->service = new BoardImportTrelloService( + $this->service = new BoardImportTrelloJsonService( $this->userManager, $this->l10n ); @@ -62,7 +62,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { ->willReturn(json_decode('{"members": [{"username": "othre_trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); - $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + $this->assertInstanceOf(BoardImportTrelloJsonService::class, $actual); } public function testValidateUsersWithNotStringNextcloud() { @@ -78,7 +78,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { ->willReturn(json_decode('{"members": [{"username": "trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); - $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + $this->assertInstanceOf(BoardImportTrelloJsonService::class, $actual); } public function testValidateUsersWithNotFoundUser() { @@ -92,7 +92,7 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { ->willReturn(json_decode('{"members": [{"username": "trello_user"}]}')); $this->service->setImportService($importService); $actual = $this->service->validateUsers(); - $this->assertInstanceOf(BoardImportTrelloService::class, $actual); + $this->assertInstanceOf(BoardImportTrelloJsonService::class, $actual); } public function testValidateUsersWithValidUsers() { @@ -123,10 +123,10 @@ class BoardImportTrelloServiceTest extends \Test\TestCase { public function testGetBoardWithSuccess() { $importService = \OC::$server->get(BoardImportService::class); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trello.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trelloJson.json')); $importService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trello.json')); + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trelloJson.json')); $importService->setConfigInstance($configInstance); $owner = $this->createMock(IUser::class); diff --git a/tests/unit/controller/BoardImportApiControllerTest.php b/tests/unit/controller/BoardImportApiControllerTest.php index 5389b46f1..ef16cab26 100644 --- a/tests/unit/controller/BoardImportApiControllerTest.php +++ b/tests/unit/controller/BoardImportApiControllerTest.php @@ -50,16 +50,23 @@ class BoardImportApiControllerTest extends \Test\TestCase { } public function testGetAllowedSystems() { + $allowedSystems = [ + [ + 'name' => '', + 'class' => '', + 'internalName' => 'trelloJson' + ] + ]; $this->boardImportService ->method('getAllowedImportSystems') - ->willReturn(['trello']); + ->willReturn($allowedSystems); $actual = $this->controller->getAllowedSystems(); - $expected = new DataResponse(['trello'], HTTP::STATUS_OK); + $expected = new DataResponse($allowedSystems, HTTP::STATUS_OK); $this->assertEquals($expected, $actual); } public function testImport() { - $system = 'trello'; + $system = 'trelloJson'; $config = [ 'owner' => 'test' ]; From e87c063076e6e00c3554c963ca60906d17de2e90 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 25 Jul 2021 00:15:50 -0300 Subject: [PATCH 14/21] Documentation, improvements on validation, refactor Validate get boad change pattern of api params Import only one board by api Populate data from api Update class diagram Update documentation Add return when success Sort comments Fix order of cards Instructions of attachments Signed-off-by: Vitor Mattos --- docs/User_documentation_en.md | 77 ++++- docs/implement-import.md | 2 +- docs/resources/BoardImport.svg | 286 ++++++++++-------- docs/resources/BoardImport.yuml | 15 +- lib/Service/ABoardImportService.php | 1 + lib/Service/BoardImportService.php | 2 +- lib/Service/BoardImportTrelloApiService.php | 164 ++++++++-- lib/Service/BoardImportTrelloJsonService.php | 15 +- .../fixtures/config-trelloApi-schema.json | 13 +- 9 files changed, 400 insertions(+), 175 deletions(-) diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 25ea8f8ee..824c80c5c 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -9,11 +9,14 @@ Project management, time management or ideation, Deck makes it easier for you to ## Using Deck Overall, Deck is easy to use. You can create boards, add users, share the Deck, work collaboratively and in real time. -1. [Create my first board](#1-create-my-first-board) -2. [Create stacks and cards](#2-create-stacks-and-cards) -3. [Handle cards options](#3-handle-cards-options) -4. [Archive old tasks](#4-archive-old-tasks) -5. [Manage your board](#5-manage-your-board) +- 1. [Create my first board](#1-create-my-first-board) +- 2. [Create stacks and cards](#2-create-stacks-and-cards) +- 3. [Handle cards options](#3-handle-cards-options) +- 4. [Archive old tasks](#4-archive-old-tasks) +- 5. [Manage your board](#5-manage-your-board) +- 6. [Import boards](#6-import-boards) + - [Trello JSON](#trello-json) + - [Trello API](#trello-api) ### 1. Create my first board In this example, we're going to create a board and share it with an other nextcloud user. @@ -69,6 +72,70 @@ The **sharing tab** allows you to add users or even groups to your boards. **Deleted objects** allows you to return previously deleted stacks or cards. The **Timeline** allows you to see everything that happened in your boards. Everything! +### 6. Import boards + +Importing can be done using the API or the `occ` `deck:import` command. + +It is possible to import from the following sources: + +#### Trello JSON + +Steps: +* Create the data file + * Access Trello + * go to the board you want to export + * Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON +* Create the configuration file +* Execute the import informing the import file path, data file and source as `Trello JSON` + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + +**Limitations**: + +Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has. + +#### Trello API + +Import using API is recommended for boards with more than 1000 actions. + +Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello. + +* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization) +* Get the ID of the board you want to import by making a request to: +https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name + + This ID you will use in the configuration file in the `board` property +* Create the configuration file + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "api": { + "key": "0cc175b9c0f1b6a831c399e269772661", + "token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33" + }, + "board": "8277e0910d750195b4487976", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + ## Search Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls. diff --git a/docs/implement-import.md b/docs/implement-import.md index cca83357d..51972c56d 100644 --- a/docs/implement-import.md +++ b/docs/implement-import.md @@ -1,4 +1,4 @@ ## Implement import * Create a new class `lib/service/BoardImportService.php` where `` is the name of the source system. -* Use the `lib/service/BoardImportTrelloService.php` class as inspiration \ No newline at end of file +* Use the `lib/service/BoardImportTrelloJsonService.php` class as inspiration \ No newline at end of file diff --git a/docs/resources/BoardImport.svg b/docs/resources/BoardImport.svg index 700538bdb..1d17034a8 100644 --- a/docs/resources/BoardImport.svg +++ b/docs/resources/BoardImport.svg @@ -4,175 +4,211 @@ - - + + G - + A0 - - - -Classes used on -board import. -Methods just to -illustrate. + + + +Classes used on +board import. +Methods just to +illustrate. A1 - -ApiController + +ApiController A2 - -BoardImportApiController - -+import() -+getAllowedSystems() -+getConfigSchema() + +BoardImportApiController + ++import() ++getAllowedSystems() ++getConfigSchema() A1->A2 - - + + A3 - -BoardImportService - -+import() -+bootstrap() -+validateSystem() -#validateConfig() -#validateData() + +BoardImportService + ++import() ++bootstrap() ++validateSystem() +#validateConfig() +#validateData() A2->A3 - - -uses + + +uses A7 - -BoardImportTrelloService + +BoardImportTrelloApiService + ++name:string A3->A7 - - -uses - - - -A8 - - - -validateSystem is -public because is -used on Api. - - - -A3->A8 - - - - -A4 - -Command - - - -A5 - -BoardImport - -+boardImportCommandService - -#configure() -#execute(input,output) - - - -A4->A5 - - - - - -A6 - -BoardImportCommandService - -+bootstrap() -+import() -+validateSystem() -#validateConfig() -#validateData() - - - -A5->A6 - - -uses - - - -A6->A3 - - - - - -A7->A3 - - -uses + + +uses A9 - - - -To create an import -to another system, -create another class -similar to this. + +BoardImportTrelloJsonService + ++name:string +#needValidateData:true - + -A7->A9 - +A3->A9 + + +uses A10 - -<<abstract>> -ABoardImportService + + + +validateSystem is +public because is +used on Api. - + + +A3->A10 + + + + +A4 + +Command + + + +A5 + +BoardImport + ++boardImportCommandService + +#configure() +#execute(input,output) + + + +A4->A5 + + + + + +A6 + +BoardImportCommandService + ++bootstrap() ++import() ++validateSystem() +#validateConfig() +#validateData() + + + +A5->A6 + + +uses + + + +A6->A3 + + + + + +A7->A3 + + +uses + + + +A8 + +<<abstract>> +ABoardImportService + +#needValidateData:false + ++needValidateData():bool + + + +A7->A8 + + +implements + + -A7->A10 - - -implements +A9->A3 + + +uses + + + +A9->A8 + + +implements + + + +A11 + + + +To create an import +to another system, +create another class +similar to this. + + + +A9->A11 + diff --git a/docs/resources/BoardImport.yuml b/docs/resources/BoardImport.yuml index 69aa222da..cbe89c829 100644 --- a/docs/resources/BoardImport.yuml +++ b/docs/resources/BoardImport.yuml @@ -5,13 +5,20 @@ // {generate:true} [note: Classes used on board import. Methods just to illustrate. {bg:cornsilk}] + [ApiController]<-[BoardImportApiController|+import();+getAllowedSystems();+getConfigSchema()] [BoardImportApiController]uses-.->[BoardImportService|+import();+bootstrap();+validateSystem();#validateConfig();#validateData();] + [Command]<-[BoardImport|+boardImportCommandService|#configure();#execute(input,output)] [BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()] [BoardImportCommandService]->[BoardImportService] -[BoardImportService]uses-.->[BoardImportTrelloService] -[BoardImportTrelloService]uses-.->[BoardImportService] + +[BoardImportService]uses-.->[BoardImportTrelloApiService|+name:string] +[BoardImportTrelloApiService]uses-.->[BoardImportService] +[BoardImportTrelloApiService]implements-.-^[<> ABoardImportService|#needValidateData:false|+needValidateData():bool] + +[BoardImportService]uses-.->[BoardImportTrelloJsonService|+name:string;#needValidateData:true] +[BoardImportTrelloJsonService]uses-.->[BoardImportService] [BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}] -[BoardImportTrelloService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] -[BoardImportTrelloService]implements-.-^[<> ABoardImportService] \ No newline at end of file +[BoardImportTrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] +[BoardImportTrelloJsonService]implements-.-^[<> ABoardImportService] diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 746febbc9..cbadca6b6 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -37,6 +37,7 @@ abstract class ABoardImportService { public static $name = ''; /** @var BoardImportService */ private $boardImportService; + /** @var bool */ protected $needValidateData = true; /** @var Stack[] */ protected $stacks = []; diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 3a35a57aa..07cfb790d 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -180,7 +180,7 @@ class BoardImportService { $className = 'OCA\Deck\Service\\'.$matches['class']; if (!class_exists($className)) { /** @psalm-suppress UnresolvableInclude */ - require_once $name; + require_once $className; } /** @psalm-suppress InvalidPropertyFetch */ $name = $className::$name; diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php index 5a8c54117..94e8f2432 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/BoardImportTrelloApiService.php @@ -23,14 +23,11 @@ namespace OCA\Deck\Service; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\RequestException; -use OCP\AppFramework\Http; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IL10N; -use OCP\ILogger; use OCP\IUserManager; +use Psr\Log\LoggerInterface; class BoardImportTrelloApiService extends BoardImportTrelloJsonService { /** @var string */ @@ -38,16 +35,17 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { protected $needValidateData = false; /** @var IClient */ private $httpClient; - /** @var ILogger */ + /** @var LoggerInterface */ protected $logger; /** @var string */ private $baseApiUrl = 'https://api.trello.com/1'; - + /** @var ?\stdClass[] */ + private $boards; public function __construct( IUserManager $userManager, IL10N $l10n, - ILogger $logger, + LoggerInterface $logger, IClientService $httpClientService ) { parent::__construct($userManager, $l10n); @@ -56,44 +54,150 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { } public function bootstrap(): void { - $this->getBoards(); + $this->populateBoard(); + $this->populateMembers(); + $this->populateLabels(); + $this->populateLists(); + $this->populateCheckLists(); + $this->populateCards(); + $this->populateActions(); parent::bootstrap(); } - private function getBoards() { - $boards = $this->doRequest('/members/me/boards'); + private function populateActions(): void { + $data = $this->getImportService()->getData(); + $data->actions = $this->doRequest( + '/boards/' . $data->id . '/actions', + [ + 'filter' => 'commentCard', + 'fields=memberCreator,type,data,date', + 'memberCreator_fields' => 'username', + 'limit' => 1000 + ] + ); } - private function doRequest($path, $queryString = []) { + private function populateCards(): void { + $data = $this->getImportService()->getData(); + $data->cards = $this->doRequest( + '/boards/' . $data->id . '/cards', + [ + 'fields' => 'id,idMembers,dateLastActivity,closed,idChecklists,name,idList,pos,desc,due,labels', + 'attachments' => true, + 'attachment_fields' => 'name,url,date', + 'limit' => 1000 + ] + ); + } + + private function populateCheckLists(): void { + $data = $this->getImportService()->getData(); + $data->checklists = $this->doRequest( + '/boards/' . $data->id . '/checkLists', + [ + 'fields' => 'id,idCard,name', + 'checkItem_fields' => 'id,state,name', + 'limit' => 1000 + ] + ); + } + + private function populateLists(): void { + $data = $this->getImportService()->getData(); + $data->lists = $this->doRequest( + '/boards/' . $data->id . '/lists', + [ + 'fields' => 'id,name,closed', + 'limit' => 1000 + ] + ); + } + + private function populateLabels(): void { + $data = $this->getImportService()->getData(); + $data->labels = $this->doRequest( + '/boards/' . $data->id . '/labels', + [ + 'fields' => 'id,color,name', + 'limit' => 1000 + ] + ); + } + + private function populateMembers(): void { + $data = $this->getImportService()->getData(); + $data->members = $this->doRequest( + '/boards/' . $data->id . '/members', + [ + 'fields' => 'username', + 'limit' => 1000 + ] + ); + } + + private function populateBoard(): void { + $toImport = $this->getImportService()->getConfig('board'); + $board = $this->doRequest( + '/boards/' . $toImport, + ['fields' => 'id,name'] + ); + if ($board instanceof \stdClass) { + $this->getImportService()->setData($board); + return; + } + throw new \Exception('Invalid board id to import'); + } + + /** + * @return array|\stdClass + */ + private function doRequest(string $path = '', array $queryString = []) { + $target = $this->baseApiUrl . $path; try { - $target = $this->baseApiUrl . $path; $result = $this->httpClient ->get($target, $this->getQueryString($queryString)) ->getBody(); - $data = json_decode($result); - } catch (ClientException $e) { - $status = $e->getCode(); - if ($status === Http::STATUS_FORBIDDEN) { - $this->logger->info($target . ' refused.', ['app' => 'deck']); - } else { - $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'deck']); + if (is_string($result)) { + $data = json_decode($result); + if (is_array($data)) { + $data = array_merge( + $data, + $this->paginate($path, $queryString, $data) + ); + } + return $data; } - } catch (RequestException $e) { - $this->logger->logException($e, [ - 'message' => 'Could not connect to ' . $target, - 'level' => ILogger::INFO, - 'app' => 'deck', - ]); + throw new \Exception('Invalid return of api'); } catch (\Throwable $e) { - $this->logger->logException($e, ['app' => 'deck']); + $this->logger->critical( + $e->getMessage(), + ['app' => 'deck'] + ); + throw new \Exception($e->getMessage()); } - return $data; } - private function getQueryString($params = []): array { + private function paginate(string $path = '', array $queryString = [], array $data = []): array { + if (empty($queryString['limit'])) { + return []; + } + if (count($data) < $queryString['limit']) { + return []; + } + $queryString['before'] = end($data)->id; + $return = $this->doRequest($path, $queryString); + if (is_array($return)) { + return $return; + } + throw new \Exception('Invalid return of api'); + } + + private function getQueryString(array $params = []): array { $apiSettings = $this->getImportService()->getConfig('api'); $params['key'] = $apiSettings->key; - $params['value'] = $apiSettings->token; - return $params; + $params['token'] = $apiSettings->token; + return [ + 'query' => $params + ]; } } diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/BoardImportTrelloJsonService.php index 9bd7f6ff4..5ea3fa429 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -111,6 +111,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $c->id; }, $values); $trelloComments = array_combine($keys, $values); + $trelloComments = $this->sortComments($trelloComments); foreach ($trelloComments as $commentId => $trelloComment) { $comment = new Comment(); if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { @@ -131,6 +132,18 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $comments; } + private function sortComments(array $comments): array { + $comparison = function($a, $b) { + if ($a->date == $b->date) { + return 0; + } + return ($a->date < $b->date) ? -1 : 1; + }; + + usort($comments, $comparison); + return $comments; + } + public function getCardLabelAssignment(): array { $cardsLabels = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { @@ -221,7 +234,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { $cardsOnStack[] = $card; $this->stacks[$trelloCard->idList]->setCards($cardsOnStack); $card->setType('plain'); - $card->setOrder($trelloCard->idShort); + $card->setOrder($trelloCard->pos); $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { diff --git a/lib/Service/fixtures/config-trelloApi-schema.json b/lib/Service/fixtures/config-trelloApi-schema.json index baef76ce5..056c9e519 100644 --- a/lib/Service/fixtures/config-trelloApi-schema.json +++ b/lib/Service/fixtures/config-trelloApi-schema.json @@ -6,20 +6,17 @@ "properties": { "key": { "type": "string", - "pattern": "^\\w{32}$" + "pattern": "^[0-9a-fA-F]{32}$" }, "token": { "type": "string", - "pattern": "^\\w{1,}$" + "pattern": "^[0-9a-fA-F]{64}$" } } }, - "boards": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\w{1,}$" - } + "board": { + "type": "string", + "pattern": "^\\w{1,}$" }, "uidRelation": { "type": "object", From 45618873486ff3c6305c81010a844ad6c35740d4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 26 Jul 2021 23:58:30 -0300 Subject: [PATCH 15/21] Add long comments with attachment Signed-off-by: Vitor Mattos --- docs/User_documentation_en.md | 19 +++++---- lib/Service/BoardImportService.php | 24 +++++++++++ lib/Service/BoardImportTrelloApiService.php | 4 +- lib/Service/BoardImportTrelloJsonService.php | 42 ++++++++++++++++++-- lib/Service/FileService.php | 2 +- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 824c80c5c..6000fba0c 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -9,14 +9,13 @@ Project management, time management or ideation, Deck makes it easier for you to ## Using Deck Overall, Deck is easy to use. You can create boards, add users, share the Deck, work collaboratively and in real time. -- 1. [Create my first board](#1-create-my-first-board) -- 2. [Create stacks and cards](#2-create-stacks-and-cards) -- 3. [Handle cards options](#3-handle-cards-options) -- 4. [Archive old tasks](#4-archive-old-tasks) -- 5. [Manage your board](#5-manage-your-board) -- 6. [Import boards](#6-import-boards) - - [Trello JSON](#trello-json) - - [Trello API](#trello-api) +1. [Create my first board](#1-create-my-first-board) +2. [Create stacks and cards](#2-create-stacks-and-cards) +3. [Handle cards options](#3-handle-cards-options) +4. [Archive old tasks](#4-archive-old-tasks) +5. [Manage your board](#5-manage-your-board) +6. [Import boards](#6-import-boards) +7. [Search](#7-search) ### 1. Create my first board In this example, we're going to create a board and share it with an other nextcloud user. @@ -136,14 +135,14 @@ Example configuration file: } ``` -## Search +### 7. Search Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls. This search allows advanced filtering of cards across all board of the logged in user. For example the search `project tag:ToDo assigned:alice assigned:bob` will return all cards where the card title or description contains project **and** the tag ToDo is set **and** the user alice is assigned **and** the user bob is assigned. -### Supported search filters +#### Supported search filters | Filter | Operators | Query | | ----------- | ----------------- | ------------------------------------------------------------ | diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 07cfb790d..5a71a91fb 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -29,6 +29,8 @@ use OCA\Deck\AppInfo\Application; use OCA\Deck\BadRequestException; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Attachment; +use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; @@ -61,6 +63,8 @@ class BoardImportService { private $cardMapper; /** @var AssignmentMapper */ private $assignmentMapper; + /** @var AttachmentMapper */ + private $attachmentMapper; /** @var ICommentsManager */ private $commentsManager; /** @var string */ @@ -96,6 +100,7 @@ class BoardImportService { LabelMapper $labelMapper, StackMapper $stackMapper, AssignmentMapper $assignmentMapper, + AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ICommentsManager $commentsManager ) { @@ -107,6 +112,7 @@ class BoardImportService { $this->stackMapper = $stackMapper; $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; + $this->attachmentMapper = $attachmentMapper; $this->commentsManager = $commentsManager; $this->board = new Board(); $this->disableCommentsEvents(); @@ -342,6 +348,24 @@ class BoardImportService { } } + public function insertAttachment(Attachment $attachment, string $content): Attachment { + $service = \OC::$server->get(FileService::class); + $folder = $service->getFolder($attachment); + + if ($folder->fileExists($attachment->getData())) { + $attachment = $this->attachmentMapper->findByData($attachment->getCardId(), $attachment->getData()); + throw new ConflictException('File already exists.', $attachment); + } + + $target = $folder->newFile($attachment->getData()); + $target->putContent($content); + + $attachment = $this->attachmentMapper->insert($attachment); + + $service->extendData($attachment); + return $attachment; + } + public function setData(\stdClass $data): void { $this->data = $data; } diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php index 94e8f2432..8cecbbd13 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/BoardImportTrelloApiService.php @@ -26,6 +26,7 @@ namespace OCA\Deck\Service; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUserManager; use Psr\Log\LoggerInterface; @@ -44,11 +45,12 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { public function __construct( IUserManager $userManager, + IURLGenerator $urlGenerator, IL10N $l10n, LoggerInterface $logger, IClientService $httpClientService ) { - parent::__construct($userManager, $l10n); + parent::__construct($userManager, $urlGenerator, $l10n); $this->logger = $logger; $this->httpClient = $httpClientService->newClient(); } diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/BoardImportTrelloJsonService.php index 5ea3fa429..2f69d7a19 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -27,11 +27,14 @@ use OC\Comments\Comment; use OCA\Deck\BadRequestException; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Assignment; +use OCA\Deck\Db\Attachment; use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; +use OCP\Comments\IComment; use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; @@ -40,6 +43,8 @@ class BoardImportTrelloJsonService extends ABoardImportService { public static $name = 'Trello JSON'; /** @var IUserManager */ private $userManager; + /** @var IURLGenerator */ + private $urlGenerator; /** @var IL10N */ private $l10n; /** @var IUser[] */ @@ -47,9 +52,11 @@ class BoardImportTrelloJsonService extends ABoardImportService { public function __construct( IUserManager $userManager, + IURLGenerator $urlGenerator, IL10N $l10n ) { $this->userManager = $userManager; + $this->urlGenerator = $urlGenerator; $this->l10n = $l10n; } @@ -113,19 +120,48 @@ class BoardImportTrelloJsonService extends ABoardImportService { $trelloComments = array_combine($keys, $values); $trelloComments = $this->sortComments($trelloComments); foreach ($trelloComments as $commentId => $trelloComment) { + $cardId = $this->cards[$trelloCard->id]->getId(); $comment = new Comment(); if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { $actor = $this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); } else { $actor = $this->getImportService()->getConfig('owner')->getUID(); } + $message = $this->replaceUsernames($trelloComment->data->text); + if (mb_strlen($message, 'UTF-8') > IComment::MAX_MESSAGE_LENGTH) { + $attachment = new Attachment(); + $attachment->setCardId($cardId); + $attachment->setType('deck_file'); + $attachment->setCreatedBy($actor); + $attachment->setLastModified(time()); + $attachment->setCreatedAt(time()); + $attachment->setData('comment_' . $commentId . '.md'); + $attachment = $this->getImportService()->insertAttachment($attachment, $message); + + $urlToDownloadAttachment = $this->urlGenerator->linkToRouteAbsolute( + 'deck.attachment.display', + [ + 'cardId' => $cardId, + 'attachmentId' => $attachment->getId() + ] + ); + $message = $this->l10n->t( + "This comment has more than %s characters.\n" . + "Added as an attachment to the card with name %s\n" . + "Accessible on URL: %s.", + [ + IComment::MAX_MESSAGE_LENGTH, + 'comment_' . $commentId . '.md', + $urlToDownloadAttachment + ] + ); + } $comment ->setActor('users', $actor) - ->setMessage($this->replaceUsernames($trelloComment->data->text), 0) + ->setMessage($message) ->setCreationDateTime( \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) ); - $cardId = $this->cards[$trelloCard->id]->getId(); $comments[$cardId][$commentId] = $comment; } } @@ -133,7 +169,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { } private function sortComments(array $comments): array { - $comparison = function($a, $b) { + $comparison = function (\stdClass $a, \stdClass $b): int { if ($a->date == $b->date) { return 0; } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 2c5e359d3..7158c6d7c 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -86,7 +86,7 @@ class FileService implements IAttachmentService { * @return ISimpleFolder * @throws NotPermittedException */ - private function getFolder(Attachment $attachment) { + public function getFolder(Attachment $attachment) { $folderName = 'file-card-' . (int)$attachment->getCardId(); try { $folder = $this->appData->getFolder($folderName); From 5b30577df06370ebb9d1911d27244ecb360c327f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 27 Jul 2021 00:00:29 -0300 Subject: [PATCH 16/21] Update documentation, import created Signed-off-by: Vitor Mattos --- docs/User_documentation_en.md | 2 ++ lib/Service/BoardImportService.php | 14 ++++++++++++ lib/Service/BoardImportTrelloApiService.php | 2 +- lib/Service/BoardImportTrelloJsonService.php | 22 +++++++++++++++++-- tests/unit/Service/BoardImportServiceTest.php | 11 +++++++--- .../Service/BoardImportTrelloServiceTest.php | 5 +++++ 6 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 6000fba0c..3a6f9dc18 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -75,6 +75,8 @@ The **Timeline** allows you to see everything that happened in your boards. Ever Importing can be done using the API or the `occ` `deck:import` command. +Comments with more than 1000 characters are placed as attached files to the card. + It is possible to import from the following sources: #### Trello JSON diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 5a71a91fb..50e395c2a 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -273,7 +273,21 @@ class BoardImportService { public function importCards(): void { $cards = $this->getImportSystem()->getCards(); foreach ($cards as $code => $card) { + $createdAt = $card->getCreatedAt(); + $lastModified = $card->getLastModified(); $this->cardMapper->insert($card); + $updateDate = false; + if ($createdAt && $createdAt !== $card->getCreatedAt()) { + $card->setCreatedAt($createdAt); + $updateDate = true; + } + if ($lastModified && $lastModified !== $card->getLastModified()) { + $card->setLastModified($lastModified); + $updateDate = true; + } + if ($updateDate) { + $this->cardMapper->update($card, false); + } $this->getImportSystem()->updateCard($code, $card); } } diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php index 8cecbbd13..301daa6d4 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/BoardImportTrelloApiService.php @@ -71,7 +71,7 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { $data->actions = $this->doRequest( '/boards/' . $data->id . '/actions', [ - 'filter' => 'commentCard', + 'filter' => 'commentCard,createCard', 'fields=memberCreator,type,data,date', 'memberCreator_fields' => 'username', 'limit' => 1000 diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/BoardImportTrelloJsonService.php index 2f69d7a19..6645f0e76 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -255,7 +255,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); $card->setLastModified($lastModified->format('Y-m-d H:i:s')); if ($trelloCard->closed) { - $card->setDeletedAt($lastModified->format('U')); + $card->setArchived(true); } if ((count($trelloCard->idChecklists) !== 0)) { foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) { @@ -272,6 +272,24 @@ class BoardImportTrelloJsonService extends ABoardImportService { $card->setType('plain'); $card->setOrder($trelloCard->pos); $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); + + $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); + $card->setLastModified($lastModified->format('U')); + + $createCardDate = array_filter( + $this->getImportService()->getData()->actions, + function (\stdClass $a) use ($trelloCard) { + return $a->type === 'createCard' && $a->data->card->id === $trelloCard->id; + } + ); + $createCardDate = current($createCardDate); + $createCardDate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $createCardDate->date); + if ($createCardDate) { + $card->setCreatedAt($createCardDate->format('U')); + } else { + $card->setCreatedAt($lastModified->format('U')); + } + $card->setDescription($trelloCard->desc); if ($trelloCard->due) { $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) @@ -365,7 +383,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { $trelloCard->desc .= "| {$this->l10n->t('File')} | {$this->l10n->t('date')} |\n"; $trelloCard->desc .= "|---|---\n"; foreach ($trelloCard->attachments as $attachment) { - $name = $attachment->name === $attachment->url ? null : $attachment->name; + $name = mb_strlen($attachment->name, 'UTF-8') ? $attachment->name : $attachment->url; $trelloCard->desc .= "| [{$name}]({$attachment->url}) | {$attachment->date} |\n"; } } diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index 49f2afcd9..a7a88e82e 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -27,6 +27,7 @@ use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\Assignment; use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; @@ -57,6 +58,8 @@ class BoardImportServiceTest extends \Test\TestCase { private $cardMapper; /** @var AssignmentMapper|MockObject */ private $assignmentMapper; + /** @var AttachmentMapper|MockObject */ + private $attachmentMapper; /** @var ICommentsManager|MockObject */ private $commentsManager; /** @var BoardImportTrelloJsonService|MockObject */ @@ -70,8 +73,9 @@ class BoardImportServiceTest extends \Test\TestCase { $this->aclMapper = $this->createMock(AclMapper::class); $this->labelMapper = $this->createMock(LabelMapper::class); $this->stackMapper = $this->createMock(StackMapper::class); - $this->cardMapper = $this->createMock(AssignmentMapper::class); - $this->assignmentMapper = $this->createMock(CardMapper::class); + $this->cardMapper = $this->createMock(CardMapper::class); + $this->assignmentMapper = $this->createMock(AssignmentMapper::class); + $this->attachmentMapper = $this->createMock(AttachmentMapper::class); $this->commentsManager = $this->createMock(ICommentsManager::class); $this->boardImportService = new BoardImportService( $this->dbConn, @@ -80,8 +84,9 @@ class BoardImportServiceTest extends \Test\TestCase { $this->aclMapper, $this->labelMapper, $this->stackMapper, - $this->cardMapper, $this->assignmentMapper, + $this->attachmentMapper, + $this->cardMapper, $this->commentsManager ); diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/BoardImportTrelloServiceTest.php index 99c8f8c2d..b8df68359 100644 --- a/tests/unit/Service/BoardImportTrelloServiceTest.php +++ b/tests/unit/Service/BoardImportTrelloServiceTest.php @@ -23,21 +23,26 @@ namespace OCA\Deck\Service; use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; class BoardImportTrelloJsonServiceTest extends \Test\TestCase { /** @var BoardImportTrelloJsonService */ private $service; + /** @var IURLGenerator */ + private $urlGenerator; /** @var IUserManager */ private $userManager; /** @var IL10N */ private $l10n; public function setUp(): void { $this->userManager = $this->createMock(IUserManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->l10n = $this->createMock(IL10N::class); $this->service = new BoardImportTrelloJsonService( $this->userManager, + $this->urlGenerator, $this->l10n ); } From fda8a03c436cc2f1a98d6afc1827b1606b8c7b53 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 27 Jul 2021 12:55:29 -0300 Subject: [PATCH 17/21] Remove unused codes Signed-off-by: Vitor Mattos --- lib/Service/BoardImportCommandService.php | 2 +- lib/Service/BoardImportService.php | 15 --------------- tests/unit/Service/BoardImportServiceTest.php | 2 -- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index a9ca01a74..2af23cdb0 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -105,7 +105,7 @@ class BoardImportCommandService extends BoardImportService { return $answer; }); $configFile = $helper->ask($this->getInput(), $this->getOutput(), $question); - $config = $this->getInput()->setOption('config', $configFile); + $this->getInput()->setOption('config', $configFile); } catch (ConflictException $e) { $this->getOutput()->writeln('Invalid config file'); $this->getOutput()->writeln(array_map(function (array $v): string { diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 50e395c2a..7b6372ddf 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -34,21 +34,16 @@ use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\StackMapper; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; -use OCP\AppFramework\Db\Entity; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException as CommentNotFoundException; -use OCP\IDBConnection; use OCP\IUserManager; class BoardImportService { - /** @var IDBConnection */ - protected $dbConn; /** @var IUserManager */ private $userManager; /** @var BoardMapper */ @@ -93,7 +88,6 @@ class BoardImportService { private $board; public function __construct( - IDBConnection $dbConn, IUserManager $userManager, BoardMapper $boardMapper, AclMapper $aclMapper, @@ -104,7 +98,6 @@ class BoardImportService { CardMapper $cardMapper, ICommentsManager $commentsManager ) { - $this->dbConn = $dbConn; $this->userManager = $userManager; $this->boardMapper = $boardMapper; $this->aclMapper = $aclMapper; @@ -253,14 +246,6 @@ class BoardImportService { $this->getBoard()->setLabels($labels); } - public function createLabel(string $title, string $color, int $boardId): Entity { - $label = new Label(); - $label->setTitle($title); - $label->setColor($color); - $label->setBoardId($boardId); - return $this->labelMapper->insert($label); - } - public function importStacks(): void { $stacks = $this->getImportSystem()->getStacks(); foreach ($stacks as $code => $stack) { diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/BoardImportServiceTest.php index a7a88e82e..9577d1654 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/BoardImportServiceTest.php @@ -67,7 +67,6 @@ class BoardImportServiceTest extends \Test\TestCase { /** @var BoardImportService|MockObject */ private $boardImportService; public function setUp(): void { - $this->dbConn = $this->createMock(IDBConnection::class); $this->userManager = $this->createMock(IUserManager::class); $this->boardMapper = $this->createMock(BoardMapper::class); $this->aclMapper = $this->createMock(AclMapper::class); @@ -78,7 +77,6 @@ class BoardImportServiceTest extends \Test\TestCase { $this->attachmentMapper = $this->createMock(AttachmentMapper::class); $this->commentsManager = $this->createMock(ICommentsManager::class); $this->boardImportService = new BoardImportService( - $this->dbConn, $this->userManager, $this->boardMapper, $this->aclMapper, From a3959e3cfcb4096308c503fd85a6602c16eb2e4b Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Wed, 15 Sep 2021 13:37:27 -0300 Subject: [PATCH 18/21] Update lib/Service/BoardImportCommandService.php Co-authored-by: Julien Veyssier Signed-off-by: Vitor Mattos --- lib/Service/BoardImportCommandService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index 2af23cdb0..3741f85cf 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -154,7 +154,7 @@ class BoardImportCommandService extends BoardImportService { } $helper = $this->getCommand()->getHelper('question'); $question = new Question( - 'Please inform a valid data json file: ', + 'Please provide a valid data json file: ', 'data.json' ); $question->setValidator(function (string $answer) { From 24c8b2f4aa0fbccbd2eaf8b8f6057705bdffe4cc Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Fri, 17 Sep 2021 06:42:06 -0300 Subject: [PATCH 19/21] Make error more specific Signed-off-by: Vitor Mattos --- lib/Service/BoardImportCommandService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/BoardImportCommandService.php index 3741f85cf..1f09853da 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/BoardImportCommandService.php @@ -80,17 +80,18 @@ class BoardImportCommandService extends BoardImportService { $config = $this->getInput()->getOption('config'); if (is_string($config)) { if (!is_file($config)) { - throw new NotFoundException('Please inform a valid config json file'); + throw new NotFoundException('It\'s not a file.'); } $config = json_decode(file_get_contents($config)); if (!$config instanceof \stdClass) { - throw new NotFoundException('Please inform a valid config json file'); + throw new NotFoundException('Failed to parse JSON.'); } $this->setConfigInstance($config); } parent::validateConfig(); return; } catch (NotFoundException $e) { + $this->getOutput()->writeln('' . $e->getMessage() . ''); $helper = $this->getCommand()->getHelper('question'); $question = new Question( 'Please inform a valid config json file: ', From f2b6934ac3717c19f16effaf4ab1f7d88769003b Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 30 Nov 2021 09:04:53 -0300 Subject: [PATCH 20/21] Move all classes to a sub-namespace Signed-off-by: Vitor Mattos --- docs/implement-import.md | 4 +- docs/resources/BoardImport.svg | 208 +++++++++--------- docs/resources/BoardImport.yuml | 14 +- lib/Command/BoardImport.php | 2 +- lib/Controller/BoardImportApiController.php | 2 +- .../{ => Importer}/ABoardImportService.php | 2 +- .../BoardImportCommandService.php | 5 +- .../{ => Importer}/BoardImportService.php | 18 +- .../Systems/TrelloApiService.php} | 4 +- .../Systems/TrelloJsonService.php} | 5 +- .../fixtures/config-trelloApi-schema.json | 0 .../fixtures/config-trelloJson-schema.json | 0 tests/unit/Command/BoardImportTest.php | 4 +- .../{ => Importer}/BoardImportServiceTest.php | 27 +-- .../Systems/TrelloJsonServiceTest.php} | 18 +- .../BoardImportApiControllerTest.php | 28 +-- 16 files changed, 168 insertions(+), 173 deletions(-) rename lib/Service/{ => Importer}/ABoardImportService.php (98%) rename lib/Service/{ => Importer}/BoardImportCommandService.php (96%) rename lib/Service/{ => Importer}/BoardImportService.php (95%) rename lib/Service/{BoardImportTrelloApiService.php => Importer/Systems/TrelloApiService.php} (98%) rename lib/Service/{BoardImportTrelloJsonService.php => Importer/Systems/TrelloJsonService.php} (98%) rename lib/Service/{ => Importer}/fixtures/config-trelloApi-schema.json (100%) rename lib/Service/{ => Importer}/fixtures/config-trelloJson-schema.json (100%) rename tests/unit/Service/{ => Importer}/BoardImportServiceTest.php (88%) rename tests/unit/Service/{BoardImportTrelloServiceTest.php => Importer/Systems/TrelloJsonServiceTest.php} (91%) diff --git a/docs/implement-import.md b/docs/implement-import.md index 51972c56d..01066ca6b 100644 --- a/docs/implement-import.md +++ b/docs/implement-import.md @@ -1,4 +1,4 @@ ## Implement import -* Create a new class `lib/service/BoardImportService.php` where `` is the name of the source system. -* Use the `lib/service/BoardImportTrelloJsonService.php` class as inspiration \ No newline at end of file +* Create a new class `lib/Service/Importer/Systems/Service.php` where `` is the name of the source system. +* Use the `lib/Service/Importer/Systems/TrelloJsonService.php` class as inspiration \ No newline at end of file diff --git a/docs/resources/BoardImport.svg b/docs/resources/BoardImport.svg index 1d17034a8..167f85f1a 100644 --- a/docs/resources/BoardImport.svg +++ b/docs/resources/BoardImport.svg @@ -4,211 +4,211 @@ - + G - + A0 - - - -Classes used on -board import. -Methods just to -illustrate. + + + +Classes used on +board import. +Methods just to +illustrate. A1 - -ApiController + +ApiController A2 - -BoardImportApiController - -+import() -+getAllowedSystems() -+getConfigSchema() + +BoardImportApiController + ++import() ++getAllowedSystems() ++getConfigSchema() A1->A2 - - + + A3 - -BoardImportService - -+import() -+bootstrap() -+validateSystem() -#validateConfig() -#validateData() + +BoardImportService + ++import() ++bootstrap() ++validateSystem() +#validateConfig() +#validateData() A2->A3 - - -uses + + +uses A7 - -BoardImportTrelloApiService - -+name:string + +TrelloApiService + ++name:string A3->A7 - - -uses + + +uses A9 - -BoardImportTrelloJsonService - -+name:string -#needValidateData:true + +TrelloJsonService + ++name:string +#needValidateData:true A3->A9 - - -uses + + +uses A10 - - - -validateSystem is -public because is -used on Api. + + + +validateSystem is +public because is +used on Api. A3->A10 - + A4 - -Command + +Command A5 - -BoardImport - -+boardImportCommandService - -#configure() -#execute(input,output) + +BoardImport + ++boardImportCommandService + +#configure() +#execute(input,output) A4->A5 - - + + A6 - -BoardImportCommandService - -+bootstrap() -+import() -+validateSystem() -#validateConfig() -#validateData() + +BoardImportCommandService + ++bootstrap() ++import() ++validateSystem() +#validateConfig() +#validateData() A5->A6 - - -uses + + +uses A6->A3 - - + + A7->A3 - - -uses + + +uses A8 - -<<abstract>> -ABoardImportService - -#needValidateData:false - -+needValidateData():bool + +<<abstract>> +ABoardImportService + +#needValidateData:false + ++needValidateData():bool A7->A8 - - -implements + + +implements A9->A3 - - -uses + + +uses A9->A8 - - -implements + + +implements A11 - - - -To create an import -to another system, -create another class -similar to this. + + + +To create an import +to another system, +create another class +similar to this. A9->A11 - + diff --git a/docs/resources/BoardImport.yuml b/docs/resources/BoardImport.yuml index cbe89c829..4c681b79f 100644 --- a/docs/resources/BoardImport.yuml +++ b/docs/resources/BoardImport.yuml @@ -13,12 +13,12 @@ [BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()] [BoardImportCommandService]->[BoardImportService] -[BoardImportService]uses-.->[BoardImportTrelloApiService|+name:string] -[BoardImportTrelloApiService]uses-.->[BoardImportService] -[BoardImportTrelloApiService]implements-.-^[<> ABoardImportService|#needValidateData:false|+needValidateData():bool] +[BoardImportService]uses-.->[TrelloApiService|+name:string] +[TrelloApiService]uses-.->[BoardImportService] +[TrelloApiService]implements-.-^[<> ABoardImportService|#needValidateData:false|+needValidateData():bool] -[BoardImportService]uses-.->[BoardImportTrelloJsonService|+name:string;#needValidateData:true] -[BoardImportTrelloJsonService]uses-.->[BoardImportService] +[BoardImportService]uses-.->[TrelloJsonService|+name:string;#needValidateData:true] +[TrelloJsonService]uses-.->[BoardImportService] [BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}] -[BoardImportTrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] -[BoardImportTrelloJsonService]implements-.-^[<> ABoardImportService] +[TrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] +[TrelloJsonService]implements-.-^[<> ABoardImportService] diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 0674b128a..15fec07b2 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -23,7 +23,7 @@ namespace OCA\Deck\Command; -use OCA\Deck\Service\BoardImportCommandService; +use OCA\Deck\Service\Importer\BoardImportCommandService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; diff --git a/lib/Controller/BoardImportApiController.php b/lib/Controller/BoardImportApiController.php index 2d2545105..ec7e76cae 100644 --- a/lib/Controller/BoardImportApiController.php +++ b/lib/Controller/BoardImportApiController.php @@ -23,7 +23,7 @@ namespace OCA\Deck\Controller; -use OCA\Deck\Service\BoardImportService; +use OCA\Deck\Service\Importer\BoardImportService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; diff --git a/lib/Service/ABoardImportService.php b/lib/Service/Importer/ABoardImportService.php similarity index 98% rename from lib/Service/ABoardImportService.php rename to lib/Service/Importer/ABoardImportService.php index cbadca6b6..68a65cbdc 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/Importer/ABoardImportService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer; use OCA\Deck\Db\Acl; use OCA\Deck\Db\Assignment; diff --git a/lib/Service/BoardImportCommandService.php b/lib/Service/Importer/BoardImportCommandService.php similarity index 96% rename from lib/Service/BoardImportCommandService.php rename to lib/Service/Importer/BoardImportCommandService.php index 1f09853da..8e25f417f 100644 --- a/lib/Service/BoardImportCommandService.php +++ b/lib/Service/Importer/BoardImportCommandService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; @@ -80,7 +80,7 @@ class BoardImportCommandService extends BoardImportService { $config = $this->getInput()->getOption('config'); if (is_string($config)) { if (!is_file($config)) { - throw new NotFoundException('It\'s not a file.'); + throw new NotFoundException('It\'s not a valid config file.'); } $config = json_decode(file_get_contents($config)); if (!$config instanceof \stdClass) { @@ -94,6 +94,7 @@ class BoardImportCommandService extends BoardImportService { $this->getOutput()->writeln('' . $e->getMessage() . ''); $helper = $this->getCommand()->getHelper('question'); $question = new Question( + "You can get more info on https://deck.readthedocs.io/en/latest/User_documentation_en/#6-import-boards\n" . 'Please inform a valid config json file: ', 'config.json' ); diff --git a/lib/Service/BoardImportService.php b/lib/Service/Importer/BoardImportService.php similarity index 95% rename from lib/Service/BoardImportService.php rename to lib/Service/Importer/BoardImportService.php index 7b6372ddf..d6de6cd80 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/Importer/BoardImportService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer; use JsonSchema\Constraints\Constraint; use JsonSchema\Validator; @@ -163,20 +163,10 @@ class BoardImportService { public function getAllowedImportSystems(): array { if (!$this->allowedSystems) { - $allowedSystems = glob(__DIR__ . '/BoardImport*Service.php'); - $allowedSystems = array_filter($allowedSystems, function (string $name) { - $name = basename($name); - switch ($name) { - case 'ABoardImportService.php': - case 'BoardImportService.php': - case 'BoardImportCommandService.php': - return false; - } - return true; - }); + $allowedSystems = glob(__DIR__ . '/Systems/*Service.php'); $allowedSystems = array_map(function ($filename) { - preg_match('/\/(?BoardImport(?\w+)Service)\.php$/', $filename, $matches); - $className = 'OCA\Deck\Service\\'.$matches['class']; + preg_match('/\/(?(?\w+)Service)\.php$/', $filename, $matches); + $className = 'OCA\Deck\Service\Importer\Systems\\'.$matches['class']; if (!class_exists($className)) { /** @psalm-suppress UnresolvableInclude */ require_once $className; diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/Importer/Systems/TrelloApiService.php similarity index 98% rename from lib/Service/BoardImportTrelloApiService.php rename to lib/Service/Importer/Systems/TrelloApiService.php index 301daa6d4..e95afb6b7 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/Importer/Systems/TrelloApiService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer\Systems; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; @@ -30,7 +30,7 @@ use OCP\IURLGenerator; use OCP\IUserManager; use Psr\Log\LoggerInterface; -class BoardImportTrelloApiService extends BoardImportTrelloJsonService { +class TrelloApiService extends TrelloJsonService { /** @var string */ public static $name = 'Trello API'; protected $needValidateData = false; diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/Importer/Systems/TrelloJsonService.php similarity index 98% rename from lib/Service/BoardImportTrelloJsonService.php rename to lib/Service/Importer/Systems/TrelloJsonService.php index 6645f0e76..49b02a71a 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/Importer/Systems/TrelloJsonService.php @@ -21,7 +21,7 @@ * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer\Systems; use OC\Comments\Comment; use OCA\Deck\BadRequestException; @@ -32,13 +32,14 @@ use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Label; use OCA\Deck\Db\Stack; +use OCA\Deck\Service\Importer\ABoardImportService; use OCP\Comments\IComment; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; -class BoardImportTrelloJsonService extends ABoardImportService { +class TrelloJsonService extends ABoardImportService { /** @var string */ public static $name = 'Trello JSON'; /** @var IUserManager */ diff --git a/lib/Service/fixtures/config-trelloApi-schema.json b/lib/Service/Importer/fixtures/config-trelloApi-schema.json similarity index 100% rename from lib/Service/fixtures/config-trelloApi-schema.json rename to lib/Service/Importer/fixtures/config-trelloApi-schema.json diff --git a/lib/Service/fixtures/config-trelloJson-schema.json b/lib/Service/Importer/fixtures/config-trelloJson-schema.json similarity index 100% rename from lib/Service/fixtures/config-trelloJson-schema.json rename to lib/Service/Importer/fixtures/config-trelloJson-schema.json diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php index 8c0c23119..32701811b 100644 --- a/tests/unit/Command/BoardImportTest.php +++ b/tests/unit/Command/BoardImportTest.php @@ -23,7 +23,7 @@ namespace OCA\Deck\Command; -use OCA\Deck\Service\BoardImportCommandService; +use OCA\Deck\Service\Importer\BoardImportCommandService; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -37,7 +37,7 @@ class BoardImportTest extends \Test\TestCase { public function setUp(): void { parent::setUp(); - $this->boardImportCommandService = $this->createMock(boardImportCommandService::class); + $this->boardImportCommandService = $this->createMock(BoardImportCommandService::class); $this->boardImport = new BoardImport( $this->boardImportCommandService ); diff --git a/tests/unit/Service/BoardImportServiceTest.php b/tests/unit/Service/Importer/BoardImportServiceTest.php similarity index 88% rename from tests/unit/Service/BoardImportServiceTest.php rename to tests/unit/Service/Importer/BoardImportServiceTest.php index 9577d1654..787dfdd5f 100644 --- a/tests/unit/Service/BoardImportServiceTest.php +++ b/tests/unit/Service/Importer/BoardImportServiceTest.php @@ -20,7 +20,7 @@ * along with this program. If not, see . * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer; use OC\Comments\Comment; use OCA\Deck\Db\Acl; @@ -35,6 +35,7 @@ use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\Importer\Systems\TrelloJsonService; use OCP\Comments\ICommentsManager; use OCP\IDBConnection; use OCP\IUser; @@ -62,8 +63,8 @@ class BoardImportServiceTest extends \Test\TestCase { private $attachmentMapper; /** @var ICommentsManager|MockObject */ private $commentsManager; - /** @var BoardImportTrelloJsonService|MockObject */ - private $importTrelloJsonService; + /** @var TrelloJsonService|MockObject */ + private $trelloJsonService; /** @var BoardImportService|MockObject */ private $boardImportService; public function setUp(): void { @@ -90,14 +91,14 @@ class BoardImportServiceTest extends \Test\TestCase { $this->boardImportService->setSystem('trelloJson'); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trelloJson.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../../data/data-trelloJson.json')); $this->boardImportService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trelloJson.json')); + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../../data/config-trelloJson.json')); $this->boardImportService->setConfigInstance($configInstance); - $this->importTrelloJsonService = $this->createMock(BoardImportTrelloJsonService::class); - $this->boardImportService->setImportSystem($this->importTrelloJsonService); + $this->trelloJsonService = $this->createMock(TrelloJsonService::class); + $this->boardImportService->setImportSystem($this->trelloJsonService); $owner = $this->createMock(IUser::class); $owner @@ -125,35 +126,35 @@ class BoardImportServiceTest extends \Test\TestCase { ->expects($this->once()) ->method('insert'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getAclList') ->willReturn([new Acl()]); $this->aclMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getLabels') ->willReturn([new Label()]); $this->labelMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getStacks') ->willReturn([new Stack()]); $this->stackMapper ->expects($this->once()) ->method('insert'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getCards') ->willReturn([new Card()]); $this->cardMapper ->expects($this->any()) ->method('insert'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getComments') ->willReturn([ 'fakecardid' => [new Comment()] @@ -162,7 +163,7 @@ class BoardImportServiceTest extends \Test\TestCase { ->expects($this->once()) ->method('save'); - $this->importTrelloJsonService + $this->trelloJsonService ->method('getCardAssignments') ->willReturn([ 'fakecardid' => [new Assignment()] diff --git a/tests/unit/Service/BoardImportTrelloServiceTest.php b/tests/unit/Service/Importer/Systems/TrelloJsonServiceTest.php similarity index 91% rename from tests/unit/Service/BoardImportTrelloServiceTest.php rename to tests/unit/Service/Importer/Systems/TrelloJsonServiceTest.php index b8df68359..6eec07a9b 100644 --- a/tests/unit/Service/BoardImportTrelloServiceTest.php +++ b/tests/unit/Service/Importer/Systems/TrelloJsonServiceTest.php @@ -20,19 +20,21 @@ * along with this program. If not, see . * */ -namespace OCA\Deck\Service; +namespace OCA\Deck\Service\Importer\Systems; +use OCA\Deck\Service\Importer\BoardImportService; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; -class BoardImportTrelloJsonServiceTest extends \Test\TestCase { - /** @var BoardImportTrelloJsonService */ +class TrelloJsonServiceTest extends \Test\TestCase { + /** @var TrelloJsonService */ private $service; - /** @var IURLGenerator */ + /** @var IURLGenerator|MockObject */ private $urlGenerator; - /** @var IUserManager */ + /** @var IUserManager|MockObject */ private $userManager; /** @var IL10N */ private $l10n; @@ -40,7 +42,7 @@ class BoardImportTrelloJsonServiceTest extends \Test\TestCase { $this->userManager = $this->createMock(IUserManager::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->l10n = $this->createMock(IL10N::class); - $this->service = new BoardImportTrelloJsonService( + $this->service = new TrelloJsonService( $this->userManager, $this->urlGenerator, $this->l10n @@ -128,10 +130,10 @@ class BoardImportTrelloJsonServiceTest extends \Test\TestCase { public function testGetBoardWithSuccess() { $importService = \OC::$server->get(BoardImportService::class); - $data = json_decode(file_get_contents(__DIR__ . '/../../data/data-trelloJson.json')); + $data = json_decode(file_get_contents(__DIR__ . '/../../../../data/data-trelloJson.json')); $importService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../data/config-trelloJson.json')); + $configInstance = json_decode(file_get_contents(__DIR__ . '/../../../../data/config-trelloJson.json')); $importService->setConfigInstance($configInstance); $owner = $this->createMock(IUser::class); diff --git a/tests/unit/controller/BoardImportApiControllerTest.php b/tests/unit/controller/BoardImportApiControllerTest.php index ef16cab26..926e4c473 100644 --- a/tests/unit/controller/BoardImportApiControllerTest.php +++ b/tests/unit/controller/BoardImportApiControllerTest.php @@ -1,23 +1,23 @@ + * @copyright Copyright (c) 2021 Vitor Mattos * - * @author Ryan Fletcher + * @author Vitor Mattos * * @license GNU AGPL version 3 or any later version * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . * */ namespace OCA\Deck\Controller; @@ -26,14 +26,14 @@ use OCA\Deck\Db\Board; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; -use OCA\Deck\Service\BoardImportService; +use OCA\Deck\Service\Importer\BoardImportService; class BoardImportApiControllerTest extends \Test\TestCase { private $appName = 'deck'; private $userId = 'admin'; /** @var BoardImportApiController */ private $controller; - /** @var BoardImportService */ + /** @var BoardImportService|MockObject */ private $boardImportService; public function setUp(): void { From ccd5bce7ea31cb14eab51a119b073a03461ad71f Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 27 Dec 2021 11:51:41 -0300 Subject: [PATCH 21/21] Replace glob by EventDispatcher Signed-off-by: Vitor Mattos --- docs/implement-import.md | 30 ++++++++++- lib/Event/ABoardImportGetAllowedEvent.php | 44 ++++++++++++++++ lib/Event/BoardImportGetAllowedEvent.php | 29 +++++++++++ lib/Service/Importer/ABoardImportService.php | 2 + .../Importer/BoardImportCommandService.php | 3 +- lib/Service/Importer/BoardImportService.php | 51 ++++++++++--------- .../Importer/Systems/TrelloApiService.php | 9 ++++ .../Importer/Systems/TrelloJsonService.php | 9 ++++ .../Importer/BoardImportServiceTest.php | 24 ++++++++- 9 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 lib/Event/ABoardImportGetAllowedEvent.php create mode 100644 lib/Event/BoardImportGetAllowedEvent.php diff --git a/docs/implement-import.md b/docs/implement-import.md index 01066ca6b..08f944e29 100644 --- a/docs/implement-import.md +++ b/docs/implement-import.md @@ -1,4 +1,32 @@ ## Implement import -* Create a new class `lib/Service/Importer/Systems/Service.php` where `` is the name of the source system. +* Create a new importer class extending `ABoardImportService` +* Create a listener for event `BoardImportGetAllowedEvent` to enable your importer. + > You can read more about listeners on [Nextcloud](https://docs.nextcloud.com/server/latest/developer_manual/basics/events.html?highlight=event#writing-a-listener) doc. + + Example: + +```php +class YourCustomImporterListener { + public function handle(Event $event): void { + if (!($event instanceof BoardImportGetAllowedEvent)) { + return; + } + + $event->getService()->addAllowedImportSystem([ + 'name' => YourCustomImporterService::$name, + 'class' => YourCustomImporterService::class, + 'internalName' => 'YourCustomImporter' + ]); + } +} +``` + * Register your listener on your `Application` class like this: +```php +$dispatcher = $this->getContainer()->query(IEventDispatcher::class); +$dispatcher->registerEventListener( + BoardImportGetAllowedEvent::class, + YourCustomImporterListener::class +); +``` * Use the `lib/Service/Importer/Systems/TrelloJsonService.php` class as inspiration \ No newline at end of file diff --git a/lib/Event/ABoardImportGetAllowedEvent.php b/lib/Event/ABoardImportGetAllowedEvent.php new file mode 100644 index 000000000..9dc4287ee --- /dev/null +++ b/lib/Event/ABoardImportGetAllowedEvent.php @@ -0,0 +1,44 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Event; + +use OCA\Deck\Service\Importer\BoardImportService; +use OCP\EventDispatcher\Event; + +abstract class ABoardImportGetAllowedEvent extends Event { + private $service; + + public function __construct(BoardImportService $service) { + parent::__construct(); + + $this->service = $service; + } + + public function getService(): BoardImportService { + return $this->service; + } +} diff --git a/lib/Event/BoardImportGetAllowedEvent.php b/lib/Event/BoardImportGetAllowedEvent.php new file mode 100644 index 000000000..323bfc87e --- /dev/null +++ b/lib/Event/BoardImportGetAllowedEvent.php @@ -0,0 +1,29 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + +namespace OCA\Deck\Event; + +class BoardImportGetAllowedEvent extends ABoardImportGetAllowedEvent { +} diff --git a/lib/Service/Importer/ABoardImportService.php b/lib/Service/Importer/ABoardImportService.php index 68a65cbdc..2e6acb605 100644 --- a/lib/Service/Importer/ABoardImportService.php +++ b/lib/Service/Importer/ABoardImportService.php @@ -92,6 +92,8 @@ abstract class ABoardImportService { abstract public function validateUsers(): void; + abstract public function getJsonSchemaPath(): string; + public function updateStack(string $id, Stack $stack): void { $this->stacks[$id] = $stack; } diff --git a/lib/Service/Importer/BoardImportCommandService.php b/lib/Service/Importer/BoardImportCommandService.php index 8e25f417f..d45c784de 100644 --- a/lib/Service/Importer/BoardImportCommandService.php +++ b/lib/Service/Importer/BoardImportCommandService.php @@ -114,8 +114,7 @@ class BoardImportCommandService extends BoardImportService { return $v['message']; }, $e->getData())); $this->getOutput()->writeln('Valid schema:'); - $schemaPath = __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; - $this->getOutput()->writeln(print_r(file_get_contents($schemaPath), true)); + $this->getOutput()->writeln(print_r(file_get_contents($this->getJsonSchemaPath()), true)); $this->getInput()->setOption('config', ''); } $this->validateConfig(); diff --git a/lib/Service/Importer/BoardImportService.php b/lib/Service/Importer/BoardImportService.php index d6de6cd80..72b8b0c28 100644 --- a/lib/Service/Importer/BoardImportService.php +++ b/lib/Service/Importer/BoardImportService.php @@ -36,11 +36,16 @@ use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Event\BoardImportGetAllowedEvent; use OCA\Deck\Exceptions\ConflictException; use OCA\Deck\NotFoundException; +use OCA\Deck\Service\FileService; +use OCA\Deck\Service\Importer\Systems\TrelloApiService; +use OCA\Deck\Service\Importer\Systems\TrelloJsonService; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException as CommentNotFoundException; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IUserManager; class BoardImportService { @@ -62,6 +67,8 @@ class BoardImportService { private $attachmentMapper; /** @var ICommentsManager */ private $commentsManager; + /** @var IEventDispatcher */ + private $eventDispatcher; /** @var string */ private $system = ''; /** @var null|ABoardImportService */ @@ -96,7 +103,8 @@ class BoardImportService { AssignmentMapper $assignmentMapper, AttachmentMapper $attachmentMapper, CardMapper $cardMapper, - ICommentsManager $commentsManager + ICommentsManager $commentsManager, + IEventDispatcher $eventDispatcher ) { $this->userManager = $userManager; $this->boardMapper = $boardMapper; @@ -107,6 +115,7 @@ class BoardImportService { $this->assignmentMapper = $assignmentMapper; $this->attachmentMapper = $attachmentMapper; $this->commentsManager = $commentsManager; + $this->eventDispatcher = $eventDispatcher; $this->board = new Board(); $this->disableCommentsEvents(); } @@ -161,29 +170,25 @@ class BoardImportService { return $this->system; } + public function addAllowedImportSystem($system): self { + $this->allowedSystems[] = $system; + return $this; + } + public function getAllowedImportSystems(): array { if (!$this->allowedSystems) { - $allowedSystems = glob(__DIR__ . '/Systems/*Service.php'); - $allowedSystems = array_map(function ($filename) { - preg_match('/\/(?(?\w+)Service)\.php$/', $filename, $matches); - $className = 'OCA\Deck\Service\Importer\Systems\\'.$matches['class']; - if (!class_exists($className)) { - /** @psalm-suppress UnresolvableInclude */ - require_once $className; - } - /** @psalm-suppress InvalidPropertyFetch */ - $name = $className::$name; - if (empty($name)) { - $name = lcfirst($matches['system']); - } - return [ - 'name' => $name, - 'class' => $className, - 'internalName' => lcfirst($matches['system']) - ]; - }, $allowedSystems); - $this->allowedSystems = array_values($allowedSystems); + $this->addAllowedImportSystem([ + 'name' => TrelloApiService::$name, + 'class' => TrelloApiService::class, + 'internalName' => 'TrelloApi' + ]); + $this->addAllowedImportSystem([ + 'name' => TrelloJsonService::$name, + 'class' => TrelloJsonService::class, + 'internalName' => 'TrelloJson' + ]); } + $this->eventDispatcher->dispatchTyped(new BoardImportGetAllowedEvent($this)); return $this->allowedSystems; } @@ -192,7 +197,7 @@ class BoardImportService { throw new NotFoundException('System to import not found'); } if (!is_object($this->systemInstance)) { - $systemClass = 'OCA\\Deck\\Service\\BoardImport' . ucfirst($this->getSystem()) . 'Service'; + $systemClass = 'OCA\\Deck\\Service\\Importer\\Systems\\' . ucfirst($this->getSystem()) . 'Service'; $this->systemInstance = \OC::$server->get($systemClass); $this->systemInstance->setImportService($this); } @@ -421,7 +426,7 @@ class BoardImportService { } public function getJsonSchemaPath(): string { - return __DIR__ . '/fixtures/config-' . $this->getSystem() . '-schema.json'; + return $this->getImportSystem()->getJsonSchemaPath(); } public function validateOwner(): void { diff --git a/lib/Service/Importer/Systems/TrelloApiService.php b/lib/Service/Importer/Systems/TrelloApiService.php index e95afb6b7..d2466ae1b 100644 --- a/lib/Service/Importer/Systems/TrelloApiService.php +++ b/lib/Service/Importer/Systems/TrelloApiService.php @@ -66,6 +66,15 @@ class TrelloApiService extends TrelloJsonService { parent::bootstrap(); } + public function getJsonSchemaPath(): string { + return implode(DIRECTORY_SEPARATOR, [ + __DIR__, + '..', + 'fixtures', + 'config-trelloApi-schema.json', + ]); + } + private function populateActions(): void { $data = $this->getImportService()->getData(); $data->actions = $this->doRequest( diff --git a/lib/Service/Importer/Systems/TrelloJsonService.php b/lib/Service/Importer/Systems/TrelloJsonService.php index 49b02a71a..6aeebd0b0 100644 --- a/lib/Service/Importer/Systems/TrelloJsonService.php +++ b/lib/Service/Importer/Systems/TrelloJsonService.php @@ -65,6 +65,15 @@ class TrelloJsonService extends ABoardImportService { $this->validateUsers(); } + public function getJsonSchemaPath(): string { + return implode(DIRECTORY_SEPARATOR, [ + __DIR__, + '..', + 'fixtures', + 'config-trelloJson-schema.json', + ]); + } + public function validateUsers(): void { if (empty($this->getImportService()->getConfig('uidRelation'))) { return; diff --git a/tests/unit/Service/Importer/BoardImportServiceTest.php b/tests/unit/Service/Importer/BoardImportServiceTest.php index 787dfdd5f..9e4229529 100644 --- a/tests/unit/Service/Importer/BoardImportServiceTest.php +++ b/tests/unit/Service/Importer/BoardImportServiceTest.php @@ -35,8 +35,10 @@ use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\Event\BoardImportGetAllowedEvent; use OCA\Deck\Service\Importer\Systems\TrelloJsonService; use OCP\Comments\ICommentsManager; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; @@ -63,6 +65,8 @@ class BoardImportServiceTest extends \Test\TestCase { private $attachmentMapper; /** @var ICommentsManager|MockObject */ private $commentsManager; + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; /** @var TrelloJsonService|MockObject */ private $trelloJsonService; /** @var BoardImportService|MockObject */ @@ -77,6 +81,7 @@ class BoardImportServiceTest extends \Test\TestCase { $this->assignmentMapper = $this->createMock(AssignmentMapper::class); $this->attachmentMapper = $this->createMock(AttachmentMapper::class); $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->boardImportService = new BoardImportService( $this->userManager, $this->boardMapper, @@ -86,18 +91,33 @@ class BoardImportServiceTest extends \Test\TestCase { $this->assignmentMapper, $this->attachmentMapper, $this->cardMapper, - $this->commentsManager + $this->commentsManager, + $this->eventDispatcher ); $this->boardImportService->setSystem('trelloJson'); + $this->eventDispatcher + ->method('dispatchTyped') + ->willReturnCallback(function (BoardImportGetAllowedEvent $event) { + $event->getService()->addAllowedImportSystem([ + 'name' => TrelloJsonService::$name, + 'class' => TrelloJsonService::class, + 'internalName' => 'trelloJson' + ]); + }); + $data = json_decode(file_get_contents(__DIR__ . '/../../../data/data-trelloJson.json')); $this->boardImportService->setData($data); - $configInstance = json_decode(file_get_contents(__DIR__ . '/../../../data/config-trelloJson.json')); + $configFile = __DIR__ . '/../../../data/config-trelloJson.json'; + $configInstance = json_decode(file_get_contents($configFile)); $this->boardImportService->setConfigInstance($configInstance); $this->trelloJsonService = $this->createMock(TrelloJsonService::class); + $this->trelloJsonService + ->method('getJsonSchemaPath') + ->willReturn($configFile); $this->boardImportService->setImportSystem($this->trelloJsonService); $owner = $this->createMock(IUser::class);