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 @@ protected function configure() { /** * @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(); - } - - 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)); - } + $this->getSystem() + ->validate($input, $output); } - 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 setSystem(string $system): void { + $this->system = $system; } - 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 @@ private function validateSystem(InputInterface $input, OutputInterface $output) $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 @@ public function __construct(BoardMapper $boardMapper, $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 @@ public function __construct(BoardMapper $boardMapper, $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\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 @@ public function __construct( AssignmentMapper $assignmentMapper, AclMapper $aclMapper, IDBConnection $connection, - IUserManager $userManager + IUserManager $userManager, + IL10N $l10n ) { $this->boardService = $boardService; $this->labelService = $labelService; @@ -104,6 +106,7 @@ public function __construct( $this->aclMapper = $aclMapper; $this->connection = $connection; $this->userManager = $userManager; + $this->l10n = $l10n; } public function validate(InputInterface $input, OutputInterface $output): void { @@ -127,7 +130,7 @@ public function import(InputInterface $input, OutputInterface $output): void { $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 @@ private function importCards(): void { } } + /** + * @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 @@ private function appendAttachmentsToDescription($trelloCard) { } } - 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\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 @@ public function setUp(): void { $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 @@ public function setUp(): void { $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 @@ public function __construct( * @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 @@ protected function configure() { '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 @@ protected function configure() { */ 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 @@ private function validateSystem(InputInterface $input, OutputInterface $output) * @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 @@ public function validate(InputInterface $input, OutputInterface $output): void; * @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\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 @@ public function __construct( $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 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 setData(\stdClass $data) { + $this->data = $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); - } + public function getData() { + return $this->data; } - 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 @@ private function validateUsers() { 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 @@ private function formulateChecklistText($checklist): string { 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 @@ private function importCards(): void { $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 @@ function ($a) use ($trelloCard) { } ); 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 @@ function ($a) use ($trelloCard) { } 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 @@ private function associateCardToLabels(\OCP\AppFramework\Db\Entity $card, $trell } } - private function importStacks(): void { + public function importStacks(): void { $this->stacks = []; foreach ($this->data->lists as $order => $list) { $stack = new Stack(); @@ -386,15 +345,15 @@ private function translateColor($color): string { } } - 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 @@ private function importLabels(): void { } } - private function setUserId(): void { + public function setUserId(): void { if (!property_exists($this->labelService, 'permissionService')) { return; } @@ -425,6 +384,6 @@ private function setUserId(): void { $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 @@ public function testExecuteWithSuccess() { $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 @@ public function testImportWithSuccess() { $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 @@ ['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 @@ protected function configure() { '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 @@ protected function configure() { * @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 @@ private function validateSystem(InputInterface $input, OutputInterface $output) * @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\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 @@ public function __construct( $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 @@ public function validateUsers() { 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 @@ private function formulateChecklistText($checklist): string { 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 @@ public function importCards(): void { $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 @@ public function importCards(): void { $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 @@ private function appendAttachmentsToDescription($trelloCard) { $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; - } - ); - 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(); + 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 + ); } - $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 @@ private function translateColor($color): string { } } - 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 @@ 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 @@ public function testExecuteWithSuccess() { ) ->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 @@ public function testExecuteWithSuccess() { ->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 @@ 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 @@ public function setUp(): void { $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 @@ public function testImportWithSuccess() { ) ->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 @@ public function setUp(): void { $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 @@ public function setUp(): void { $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 public function getAclList(): array; /** * @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 @@ 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 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\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 @@ public function __construct( AclMapper $aclMapper, LabelMapper $labelMapper, StackMapper $stackMapper, + AssignmentMapper $assignmentMapper, CardMapper $cardMapper, ICommentsManager $commentsManager ) { @@ -98,6 +100,7 @@ public function __construct( $this->labelMapper = $labelMapper; $this->stackMapper = $stackMapper; $this->cardMapper = $cardMapper; + $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; } @@ -149,9 +152,9 @@ public function getSystem() { 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 @@ public function getImportSystem(): ABoardImportService { 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 @@ 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 @@ private function appendAttachmentsToDescription($trelloCard) { 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 @@ public function importParticipants(): 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 @@ 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 @@ ['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 @@ protected function interact(InputInterface $input, OutputInterface $output) { $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 @@ public function __construct( * @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 @@ 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 public function updateCard(string $id, Card $card): self; 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 @@ public function getOutput(): OutputInterface { 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 @@ private function validateConfig(): void { 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 @@ private function validateData(): self { $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\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 @@ public function __construct( } 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'); + 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()); } - $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'); - } + public function validate() { + $this->validateSystem(); + $this->validateConfig(); + $this->validateUsers(); + } - if (empty($config)) { - throw new BadRequestException('config must be provided'); + protected function validateSystem() { + if (!in_array($this->getSystem(), $this->getAllowedImportSystems())) { + throw new NotFoundException('Invalid system'); } - return $this; } public function setSystem(string $system): self { @@ -162,10 +154,11 @@ public function getAllowedImportSystems(): array { } 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 @@ public function getImportSystem(): ABoardImportService { 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 @@ public function importAcl(): self { 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 @@ public function createLabel($title, $color, $boardId): Label { 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 @@ public function importComments(): self { 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 @@ public function setConfig(string $configName, $value): self { * @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 @@ public function getConfig(string $configName = null) { 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\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 @@ public function validateUsers(): self { 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 @@ public function getCards(): array { $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 @@ public function getCards(): array { 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 @@ private function appendAttachmentsToDescription($trelloCard) { 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 @@ public function importParticipants(): 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 @@ function ($a) use ($trelloCard) { ); } } - return $this; } private function replaceUsernames($text) { @@ -320,13 +313,16 @@ private function translateColor($color): string { 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 @@ public function importLabels(): self { ); $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 @@ public function setUp(): void { 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 @@ public function setUp(): void { $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 @@ public function setUp(): void { $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 | 43 ++++--- lib/Service/BoardImportService.php | 114 +++++++++++------- lib/Service/BoardImportTrelloService.php | 52 ++++---- tests/unit/Service/BoardImportServiceTest.php | 25 ++++ .../Service/BoardImportTrelloServiceTest.php | 107 +++++++--------- 10 files changed, 243 insertions(+), 174 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 @@ ['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\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 @@ public function __construct( * @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 @@ public function import($system, $config, $data) { * @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 public function getStacks(): array; */ 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\Question; class BoardImportCommandService extends BoardImportService { - /** @var Command */ + /** + * @var Command + * @psalm-suppress PropertyNotSetInConstructor + */ private $command; - /** @var InputInterface */ + /** + * @var InputInterface + * @psalm-suppress PropertyNotSetInConstructor + */ private $input; - /** @var OutputInterface */ - private $output; /** - * Data object created from config JSON - * - * @var \StdClass + * @var OutputInterface + * @psalm-suppress PropertyNotSetInConstructor */ - public $config; + private $output; /** * Define Command instance @@ -56,14 +58,11 @@ public function setCommand(Command $command): void { $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 @@ public function getInput(): InputInterface { return $this->input; } - public function setOutput($output): self { + public function setOutput(OutputInterface $output): self { $this->output = $output; return $this; } @@ -81,12 +80,12 @@ public function getOutput(): OutputInterface { 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 @@ protected function validateConfig() { '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 @@ protected function validateConfig() { $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 @@ protected function validateConfig() { return; } - protected function validateSystem() { + public function validateSystem(): void { try { parent::validateSystem(); return; @@ -142,13 +141,13 @@ protected function validateSystem() { 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 @@ 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\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 @@ public function __construct( $this->cardMapper = $cardMapper; $this->assignmentMapper = $assignmentMapper; $this->commentsManager = $commentsManager; + $this->setData(new \stdClass()); } public function import(): void { @@ -120,31 +129,35 @@ public function import(): void { } } - 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 @@ public function getAllowedImportSystems(): array { 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 @@ public function getImportSystem(): ABoardImportService { 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 @@ public function importLabels(): array { 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 @@ public function importCards(): self { return $this; } + /** + * @param mixed $cardId + * @param mixed $labelId + * @return self + */ public function assignCardToLabel($cardId, $labelId): self { $this->cardMapper->assignLabel( $cardId, @@ -248,18 +268,16 @@ public function assignCardToLabel($cardId, $labelId): self { 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 @@ public function insertComment($cardId, $comment) { 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 @@ public function insertComment($cardId, $comment) { } } - 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 @@ public function getData() { * * @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 @@ public function setConfig(string $configName, $value): self { * @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 @@ protected function validateConfig() { $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\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 @@ public function __construct( $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 @@ public function validateUsers(): self { 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 @@ public function validateUsers(): self { $user = current($user); $this->members[$user->id] = $this->getImportService()->getConfig('uidRelation')->$trelloUid; } - return $this; } /** @@ -113,7 +111,7 @@ public function getAclList(): array { return $return; } - private function checklistItem($item): string { + private function checklistItem(\stdClass $item): string { if (($item->state == 'incomplete')) { $string_start = '- [ ]'; } else { @@ -123,7 +121,7 @@ private function checklistItem($item): string { 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 @@ public function getCards(): array { 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 @@ private function appendAttachmentsToDescription($trelloCard): self { $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 @@ public function importParticipants(): self { $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 @@ function ($a) use ($trelloCard) { \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 @@ public function assignCardsToLabels(): self { ); } } - return $this; } /** @@ -279,12 +270,11 @@ public function getStacks(): array { 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 @@ private function translateColor($color): string { 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\StackMapper; use OCP\Comments\ICommentsManager; use OCP\IDBConnection; +use OCP\IUser; use OCP\IUserManager; class BoardImportServiceTest extends \Test\TestCase { @@ -83,8 +84,32 @@ public function testImportSuccess() { $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 @@ public function testValidateUsersWithoutUsers() { $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 @@ public function testValidateUsersWithInvalidUser() { ]); $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 @@ public function testValidateUsersWithNotStringNextcloud() { ]); $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 @@ public function testValidateUsersWithNotFoundUser() { $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 @@ public function testValidateUsersWithValidUsers() { $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 @@ protected function configure() { ; } - /** - * @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 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->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 @@ public function import(string $system, array $config, array $data): DataResponse $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 public function importLabels(): array; 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 @@ public function getOutput(): OutputInterface { 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 @@ protected function validateConfig(): void { 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 @@ protected function validateConfig(): void { $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 @@ public function validateSystem(): void { 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); - } - $this->setData(json_decode(file_get_contents($filename))); - if (!$this->getData()) { - $this->getOutput()->writeln('Is not a json file: ' . $filename . ''); - $this->validateData(); + 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; + } } - 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 @@ public function __construct( $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 @@ public function import(): void { } } - 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 @@ public function importParticipants(): void { $this->getImportSystem()->importParticipants(); } - final public function setData(\stdClass $data): void { + public function setData(\stdClass $data): void { $this->data = $data; } @@ -355,7 +349,7 @@ public function setConfig(string $configName, $value): void { * @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 @@ public function getConfig(string $configName = null) { } /** - * @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 @@ public function validateOwner(): void { $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 @@ public function __construct( $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 @@ public function setUp(): void { $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 @@ public function testImportSuccess() { } 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 | 72 ++++- lib/Service/BoardImportCommandService.php | 11 +- lib/Service/BoardImportService.php | 119 +++---- lib/Service/BoardImportTrelloService.php | 306 +++++++++--------- tests/data/config-trello.json | 2 +- tests/unit/Service/BoardImportServiceTest.php | 119 +++++-- .../Service/BoardImportTrelloServiceTest.php | 46 ++- 8 files changed, 379 insertions(+), 300 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 public function getStacks(): array; */ abstract public function getCards(): array; - abstract public function updateStack(string $id, Stack $stack): void; - - abstract public function updateCard(string $id, Card $card): void; + abstract public function getCardAssignments(): array; - abstract public function importParticipants(): void; + abstract public function getCardLabelAssignment(): array; - abstract public function importComments(): void; + /** + * @return IComment[][]|array + */ + abstract public function getComments(): array; /** @return Label[] */ - abstract public function importLabels(): array; + abstract public function getLabels(): array; - abstract public function assignCardsToLabels(): void; + abstract public function validateUsers(): void; - /** - * Configure import service - * - * @return void - */ - abstract public function bootstrap(): 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 @@ protected function validateConfig(): void { $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 @@ public function import(): void { $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 @@ 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 @@ public function __construct( $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 @@ public function import(): void { $this->importCards(); $this->assignCardsToLabels(); $this->importComments(); - $this->importParticipants(); + $this->importCardAssignments(); } catch (\Throwable $th) { throw new BadRequestException($th->getMessage()); } @@ -171,15 +183,14 @@ public function getAllowedImportSystems(): array { } 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 @@ public function setImportSystem(ABoardImportService $instance): void { $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 @@ public function importBoard(): void { } } - 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 @@ public function createLabel(string $title, string $color, int $boardId): Entity 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 @@ public function assignCardToLabel($cardId, $labelId): self { } 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 @@ public function insertComment(string $cardId, Comment $comment): void { } 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 @@ public function insertComment(string $cardId, Comment $comment): void { } } - 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\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,107 +79,8 @@ public function validateUsers(): void { } } - /** - * @return Acl[] - */ - public function getAclList(): array { - $return = []; - foreach ($this->members as $member) { - if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { - continue; - } - $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; - } - - 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[] - */ - public function getCards(): array { - $checklists = []; - foreach ($this->getImportService()->getData()->checklists as $checklist) { - $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); - } - $this->getImportService()->getData()->checklists = $checklists; - - 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')); - if ($trelloCard->closed) { - $card->setDeletedAt($lastModified->format('U')); - } - if ((count($trelloCard->idChecklists) !== 0)) { - foreach ($this->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) { - $trelloCard->desc .= "\n" . $checklist; - } - } - $this->appendAttachmentsToDescription($trelloCard); - - $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()); - $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); - } - $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 { + public function getCardAssignments(): array { + $assignments = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { foreach ($trelloCard->idMembers as $idMember) { if (empty($this->members[$idMember])) { @@ -204,56 +90,84 @@ public function importParticipants(): void { $assignment->setCardId($this->cards[$trelloCard->id]->getId()); $assignment->setParticipant($this->members[$idMember]->getUID()); $assignment->setType(Assignment::TYPE_USER); - $this->getImportService()->insertAssignment($assignment); + $assignments[$trelloCard->id][] = $assignment; } } + return $assignments; } - public function importComments(): void { + public function getComments(): array { + $comments = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { - $comments = array_filter( + $values = 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) { + $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 = 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 - ); + $cardId = $this->cards[$trelloCard->id]->getId(); + $comments[$cardId][$commentId] = $comment; } } + return $comments; } - private function replaceUsernames(string $text): string { - foreach ($this->getImportService()->getConfig('uidRelation') as $trello => $nextcloud) { - $text = str_replace($trello, $nextcloud->getUID(), $text); + 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 $text; + return $cardsLabels; } - 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() - ); + 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; } /** @@ -274,8 +188,69 @@ public function getStacks(): array { return $return; } - public function updateStack(string $id, Stack $stack): void { - $this->stacks[$id] = $stack; + /** + * @return Card[] + */ + public function getCards(): array { + $checklists = []; + foreach ($this->getImportService()->getData()->checklists as $checklist) { + $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); + } + $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); + $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->getImportService()->getData()->checklists[$trelloCard->id] as $checklist) { + $trelloCard->desc .= "\n" . $checklist; + } + } + $this->appendAttachmentsToDescription($trelloCard); + + $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()); + $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); + } + $cards[$trelloCard->id] = $card; + } + return $cards; + } + + /** + * @return Acl[] + */ + public function getAclList(): array { + $return = []; + foreach ($this->members as $member) { + if ($member->getUID() === $this->getImportService()->getConfig('owner')->getUID()) { + continue; + } + $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; } private function translateColor(string $color): string { @@ -305,31 +280,42 @@ private function translateColor(string $color): string { } } - 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 @@ public function setUp(): void { $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); - $actual = $this->boardImportService->import(); - $this->assertNull($actual); - } + $this->boardMapper + ->expects($this->once()) + ->method('insert'); - 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()); - } + $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(); - 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 @@ public function testValidateUsersWithValidUsers() { $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 @@ public function __construct( */ protected function configure() { $allowedSystems = $this->boardImportCommandService->getAllowedImportSystems(); + $names = array_column($allowedSystems, 'name'); $this ->setName('deck:import') ->setDescription('Import data') @@ -52,8 +53,8 @@ protected function configure() { '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 @@ protected function configure() { ->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\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 @@ public function setImportService(BoardImportService $service): void { 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 @@ public function validateSystem(): void { } 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 @@ public function import(): void { } 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 @@ public function getAllowedImportSystems(): array { } 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\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 @@ public function testExecuteWithSuccess() { ['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 @@ public function setUp(): void { $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 @@ public function testImportSuccess() { ->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 @@ public function testImportSuccess() { ->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\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 @@ public function testValidateUsersWithInvalidUser() { ->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 @@ public function testValidateUsersWithNotStringNextcloud() { ->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 @@ public function testValidateUsersWithNotFoundUser() { ->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 @@ public function testGetBoardWithNoName() { 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 @@ public function setUp(): void { } 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 | 246 ++++++++++-------- 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, 380 insertions(+), 155 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 + + +uses - - -A8 - - - -validateSystem is -public because is -used on Api. - - - -A3->A8 - + + +A9 + +BoardImportTrelloJsonService + ++name:string +#needValidateData:true + + + +A3->A9 + + +uses + + + +A10 + + + +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 - - -A9 - - - -To create an import -to another system, -create another class -similar to this. - - - -A7->A9 - - - - -A10 - -<<abstract>> -ABoardImportService + + +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 @@ public function getAllowedImportSystems(): array { $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 @@ public function __construct( } 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 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 doRequest($path, $queryString = []) { + 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()); + } + } + + 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; } - return $data; + throw new \Exception('Invalid return of api'); } - private function getQueryString($params = []): array { + 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 @@ function (\stdClass $a) use ($trelloCard) { 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 @@ function (\stdClass $a) use ($trelloCard) { 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 @@ public function getCards(): array { $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\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 @@ public function __construct( LabelMapper $labelMapper, StackMapper $stackMapper, AssignmentMapper $assignmentMapper, + AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ICommentsManager $commentsManager ) { @@ -107,6 +112,7 @@ public function __construct( $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 @@ public function importCardAssignments(): void { } } + 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 @@ 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 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 @@ function (\stdClass $a) use ($trelloCard) { $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 @@ function (\stdClass $a) use ($trelloCard) { } 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 @@ private function getFileForAttachment(Attachment $attachment) { * @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 @@ public function importStacks(): void { 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 @@ private function populateActions(): void { $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 @@ public function getCards(): array { $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 @@ public function getCards(): array { $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 @@ private function appendAttachmentsToDescription(\stdClass $trelloCard): void { $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\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 @@ public function setUp(): void { $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 @@ public function setUp(): void { $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 @@ protected function validateConfig(): void { 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\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 @@ public function __construct( CardMapper $cardMapper, ICommentsManager $commentsManager ) { - $this->dbConn = $dbConn; $this->userManager = $userManager; $this->boardMapper = $boardMapper; $this->aclMapper = $aclMapper; @@ -253,14 +246,6 @@ public function importLabels(): void { $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 @@ public function setUp(): void { $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 @@ protected function validateData(): void { } $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 @@ protected function validateConfig(): void { $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 @@ protected function validateConfig(): void { $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 @@ protected function validateConfig(): void { $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 @@ public function getSystem(): string { 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\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\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\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 @@ public function setUp(): void { $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 @@ public function testImportSuccess() { ->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 @@ public function testImportSuccess() { ->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 @@ 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->service = new TrelloJsonService( $this->userManager, $this->urlGenerator, $this->l10n @@ -128,10 +130,10 @@ public function testGetBoardWithNoName() { 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 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 public function getLabels(): array; 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 @@ protected function validateConfig(): void { 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\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 @@ public function __construct( AssignmentMapper $assignmentMapper, AttachmentMapper $attachmentMapper, CardMapper $cardMapper, - ICommentsManager $commentsManager + ICommentsManager $commentsManager, + IEventDispatcher $eventDispatcher ) { $this->userManager = $userManager; $this->boardMapper = $boardMapper; @@ -107,6 +115,7 @@ public function __construct( $this->assignmentMapper = $assignmentMapper; $this->attachmentMapper = $attachmentMapper; $this->commentsManager = $commentsManager; + $this->eventDispatcher = $eventDispatcher; $this->board = new Board(); $this->disableCommentsEvents(); } @@ -161,29 +170,25 @@ public function getSystem(): string { 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 @@ public function getImportSystem(): ABoardImportService { 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 @@ protected function validateConfig(): void { } 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 @@ public function bootstrap(): void { 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 @@ public function bootstrap(): void { $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\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 @@ public function setUp(): void { $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 @@ public function setUp(): void { $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);