diff --git a/appinfo/info.xml b/appinfo/info.xml index cf8d707e2..0bf8183e6 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -44,6 +44,7 @@ OCA\Deck\Command\UserExport + OCA\Deck\Command\BoardImport diff --git a/composer.json b/composer.json index 7bf463588..88d40fc46 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ } ], "require": { - "cogpowered/finediff": "0.3.*" + "cogpowered/finediff": "0.3.*", + "justinrainbow/json-schema": "^5.2" }, "require-dev": { "roave/security-advisories": "dev-master", diff --git a/composer.lock b/composer.lock index 08f9378c0..64256a599 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f6d91406db4e7e16e31113951986e13", + "content-hash": "cf4fb1b424f5f0c36ecc1391b10de59c", "packages": [ { "name": "cogpowered/finediff", @@ -60,6 +60,76 @@ "source": "https://github.com/cogpowered/FineDiff/tree/master" }, "time": "2014-05-19T10:25:02+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.11", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ab6744b7296ded80f8cc4f9509abbff393399aa", + "reference": "2ab6744b7296ded80f8cc4f9509abbff393399aa", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/5.2.11" + }, + "time": "2021-07-22T09:24:00+00:00" } ], "packages-dev": [ @@ -3677,16 +3747,16 @@ }, { "name": "symfony/console", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4" + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", + "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", "shasum": "" }, "require": { @@ -3756,7 +3826,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.1" + "source": "https://github.com/symfony/console/tree/v5.4.2" }, "funding": [ { @@ -3772,7 +3842,7 @@ "type": "tidelift" } ], - "time": "2021-12-09T11:22:43+00:00" + "time": "2021-12-20T16:11:12+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4070,16 +4140,16 @@ }, { "name": "symfony/finder", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "d2f29dac98e96a98be467627bd49c2efb1bc2590" + "reference": "e77046c252be48c48a40816187ed527703c8f76c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/d2f29dac98e96a98be467627bd49c2efb1bc2590", - "reference": "d2f29dac98e96a98be467627bd49c2efb1bc2590", + "url": "https://api.github.com/repos/symfony/finder/zipball/e77046c252be48c48a40816187ed527703c8f76c", + "reference": "e77046c252be48c48a40816187ed527703c8f76c", "shasum": "" }, "require": { @@ -4113,7 +4183,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.0" + "source": "https://github.com/symfony/finder/tree/v5.4.2" }, "funding": [ { @@ -4129,7 +4199,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-15T11:06:13+00:00" }, { "name": "symfony/options-resolver", @@ -4767,16 +4837,16 @@ }, { "name": "symfony/process", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5be20b3830f726e019162b26223110c8f47cf274" + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5be20b3830f726e019162b26223110c8f47cf274", - "reference": "5be20b3830f726e019162b26223110c8f47cf274", + "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", "shasum": "" }, "require": { @@ -4809,7 +4879,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.0" + "source": "https://github.com/symfony/process/tree/v5.4.2" }, "funding": [ { @@ -4825,7 +4895,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-27T21:01:00+00:00" }, { "name": "symfony/service-contracts", @@ -4974,16 +5044,16 @@ }, { "name": "symfony/string", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d" + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", + "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", "shasum": "" }, "require": { @@ -5040,7 +5110,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.0" + "source": "https://github.com/symfony/string/tree/v5.4.2" }, "funding": [ { @@ -5056,7 +5126,7 @@ "type": "tidelift" } ], - "time": "2021-11-24T10:02:00+00:00" + "time": "2021-12-16T21:52:00+00:00" }, { "name": "theseer/tokenizer", diff --git a/lib/Command/BoardImport.php b/lib/Command/BoardImport.php index 296c08bf8..0870c52a1 100644 --- a/lib/Command/BoardImport.php +++ b/lib/Command/BoardImport.php @@ -1,8 +1,8 @@ + * @copyright Copyright (c) 2021 Vitor Mattos * - * @author Julius Härtl + * @author Vitor Mattos * * @license GNU AGPL version 3 or any later version * @@ -23,80 +23,37 @@ namespace OCA\Deck\Command; -use JsonSchema\Validator; -use OCA\Deck\Db\AssignmentMapper; -use OCA\Deck\Db\Board; -use OCA\Deck\Db\BoardMapper; -use OCA\Deck\Db\Card; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Db\Stack; -use OCA\Deck\Db\StackMapper; -use OCA\Deck\Service\BoardService; -use OCA\Deck\Service\LabelService; -use OCA\Deck\Service\PermissionService; -use OCA\Deck\Service\StackService; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\IGroupManager; -use OCP\IUserManager; -use OCP\IUserSession; +use OCA\Deck\Command\Helper\ImportInterface; +use OCA\Deck\Command\Helper\TrelloHelper; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; class BoardImport extends Command { - /** @var BoardService */ - private $boardService; - // protected $cardMapper; - /** @var LabelService */ - private $labelService; - /** @var StackMapper */ - private $stackMapper; - /** @var CardMapper */ - private $cardMapper; - /** @var IUserManager */ - private $userManager; - // /** @var IGroupManager */ - // private $groupManager; - // private $assignedUsersMapper; + /** @var string */ + private $system; private $allowedSystems = ['trello']; - /** @var Board */ - private $board; + /** @var TrelloHelper */ + private $trelloHelper; + /** + * Data object created from settings JSON + * + * @var \StdClass + */ + public $settings; public function __construct( - // BoardMapper $boardMapper, - BoardService $boardService, - LabelService $labelService, - StackMapper $stackMapper, - CardMapper $cardMapper, - // IUserSession $userSession, - // StackMapper $stackMapper, - // CardMapper $cardMapper, - // AssignmentMapper $assignedUsersMapper, - IUserManager $userManager - // IGroupManager $groupManager + TrelloHelper $trelloHelper ) { parent::__construct(); - - // $this->cardMapper = $cardMapper; - $this->boardService = $boardService; - $this->labelService = $labelService; - $this->stackMapper = $stackMapper; - $this->cardMapper = $cardMapper; - - // $this->userSession = $userSession; - // $this->stackMapper = $stackMapper; - // $this->assignedUsersMapper = $assignedUsersMapper; - // $this->boardMapper = $boardMapper; - - $this->userManager = $userManager; - // $this->groupManager = $groupManager; + $this->trelloHelper = $trelloHelper; } + /** + * @return void + */ protected function configure() { $this ->setName('deck:import') @@ -127,73 +84,35 @@ class BoardImport extends Command { /** * @inheritDoc + * + * @return void */ - protected function interact(InputInterface $input, OutputInterface $output) - { + protected function interact(InputInterface $input, OutputInterface $output) { $this->validateSystem($input, $output); - $this->validateData($input, $output); - $this->validateSettings($input, $output); - $this->validateUsers(); - $this->validateOwner(); + $this->getSystem() + ->validate($input, $output); } - public function validateData(InputInterface $input, OutputInterface $output) { - $filename = $input->getOption('data'); - if (!is_file($filename)) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid data json file: ', - 'data.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Data file not found' - ); - } - return $answer; - }); - $data = $helper->ask($input, $output, $question); - $input->setOption('data', $data); - } - $this->data = json_decode(file_get_contents($filename)); - if (!$this->data) { - $output->writeln('Is not a json file: ' . $filename . ''); - $this->validateData($input, $output); - } - if (!$this->data) { - $this->data = json_decode(file_get_contents($filename)); - } + private function setSystem(string $system): void { + $this->system = $system; } - private function validateOwner() { - $this->settings->owner = $this->userManager->get($this->settings->owner); - if (!$this->settings->owner) { - throw new \LogicException('Owner "' . $this->settings->owner . '" not found on Nextcloud. Check setting json.'); - } - } - - private function validateUsers() { - if (empty($this->settings->uidRelation)) { - return; - } - foreach ($this->settings->uidRelation as $trelloUid => $nextcloudUid) { - $user = array_filter($this->data->members, fn($u) => $u->username === $trelloUid); - if (!$user) { - throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); - } - if (!is_string($nextcloudUid)) { - throw new \LogicException('User on setting uidRelation must be a string'); - } - $this->settings->uidRelation->$trelloUid = $this->userManager->get($nextcloudUid); - if (!$this->settings->uidRelation->$trelloUid) { - throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); - } - } + /** + * @return ImportInterface + */ + private function getSystem() { + $helper = $this->{$this->system . 'Helper'}; + $helper->setCommand($this); + return $helper; } + /** + * @return void + */ private function validateSystem(InputInterface $input, OutputInterface $output) { - if (in_array($input->getOption('system'), $this->allowedSystems)) { + $system = $input->getOption('system'); + if (in_array($system, $this->allowedSystems)) { + $this->setSystem($system); return; } $helper = $this->getHelper('question'); @@ -205,220 +124,19 @@ class BoardImport extends Command { $question->setErrorMessage('System %s is invalid.'); $system = $helper->ask($input, $output, $question); $input->setOption('system', $system); - } - - private function validateSettings(InputInterface $input, OutputInterface $output) { - if (!is_file($input->getOption('setting'))) { - $helper = $this->getHelper('question'); - $question = new Question( - 'Please inform a valid setting json file: ', - 'config.json' - ); - $question->setValidator(function ($answer) { - if (!is_file($answer)) { - throw new \RuntimeException( - 'Setting file not found' - ); - } - return $answer; - }); - $setting = $helper->ask($input, $output, $question); - $input->setOption('setting', $setting); - } - - $this->settings = json_decode(file_get_contents($input->getOption('setting'))); - $validator = new Validator(); - $validator->validate( - $this->settings, - (object)['$ref' => 'file://' . realpath(__DIR__ . '/fixtures/setting-schema.json')] - ); - if (!$validator->isValid()) { - $output->writeln('Invalid setting file'); - $output->writeln(array_map(fn($v) => $v['message'], $validator->getErrors())); - $output->writeln('Valid schema:'); - $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); - $input->setOption('setting', null); - $this->validateSettings($input, $output); - } + $this->setSystem($system); } /** * @param InputInterface $input * @param OutputInterface $output - * @return void - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - * @throws \ReflectionException + * + * @return int */ - protected function execute(InputInterface $input, OutputInterface $output) { - // $this->boardService->setUserId($this->settings->owner->getUID()); - $this->setUserId($this->settings->owner->getUID()); - // $this->userSession->setUser($this->settings->owner); - $this->importBoard(); - $this->importLabels(); - $this->importStacks(); - $this->importCards(); - // $boards = $this->boardService->findAll(); - - // $data = []; - // foreach ($boards as $board) { - // $fullBoard = $this->boardMapper->find($board->getId(), true, true); - // $data[$board->getId()] = (array)$fullBoard->jsonSerialize(); - // $stacks = $this->stackMapper->findAll($board->getId()); - // foreach ($stacks as $stack) { - // $data[$board->getId()]['stacks'][] = (array)$stack->jsonSerialize(); - // $cards = $this->cardMapper->findAllByStack($stack->getId()); - // foreach ($cards as $card) { - // $fullCard = $this->cardMapper->find($card->getId()); - // $assignedUsers = $this->assignedUsersMapper->findAll($card->getId()); - // $fullCard->setAssignedUsers($assignedUsers); - // $data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = (array)$fullCard->jsonSerialize(); - // } - // } - // } - // $output->writeln(json_encode($data, JSON_PRETTY_PRINT)); - return self::SUCCESS; - } - - private function checklistItem($item) { - if (($item->state == 'incomplete')) { - $string_start = '- [ ]'; - } else { - $string_start = '- [x]'; - } - $check_item_string = $string_start . ' ' . $item->name . "\n"; - return $check_item_string; - } - - function formulateChecklistText($checklist) { - $checklist_string = "\n\n## {$checklist->name}\n"; - foreach ($checklist->checkItems as $item) { - $checklist_item_string = $this->checklistItem($item); - $checklist_string = $checklist_string . "\n" . $checklist_item_string; - } - return $checklist_string; - } - - private function importCards() { - # Save checklist content into a dictionary (_should_ work even if a card has multiple checklists - foreach ($this->data->checklists as $checklist) { - $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); - } - $this->data->checklists = $checklists; - - foreach ($this->data->cards as $trelloCard) { - # Check whether a card is archived, if true, skipping to the next card - if ($trelloCard->closed) { - continue; - } - if ((count($trelloCard->idChecklists) !== 0)) { - foreach ($this->data->checklists[$trelloCard->id] as $checklist) { - $trelloCard->desc .= "\n" . $checklist; - } - } - - $card = new Card(); - $card->setTitle($trelloCard->name); - $card->setStackId($this->stacks[$trelloCard->idList]); - $card->setType('plain'); - $card->setOrder($trelloCard->idShort); - $card->setOwner($this->settings->owner->getUID()); - $card->setDescription($trelloCard->desc); - if ($trelloCard->due) { - $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.000\Z', $trelloCard->due) - ->format('Y-m-d H:i:s'); - $card->setDuedate($duedate); - } - $card = $this->cardMapper->insert($card); - - $this->associateCardToLabels($card->getId(), $trelloCard); - } - } - - public function associateCardToLabels($cardId, $card) { - foreach ($card->labels as $label) { - $this->cardMapper->assignLabel( - $cardId, - $this->labels[$label->id]->getId() - ); - } - } - - private function importStacks() { - $this->stacks = []; - foreach ($this->data->lists as $order => $list) { - if ($list->closed) { - continue; - } - $stack = new Stack(); - $stack->setTitle($list->name); - $stack->setBoardId($this->board->getId()); - $stack->setOrder($order + 1); - $stack = $this->stackMapper->insert($stack); - $this->stacks[$list->id] = $stack; - } - } - - private function translateColor($color) { - switch ($color) { - case 'red': - return 'ff0000'; - case 'yellow': - return 'ffff00'; - case 'orange': - return 'ff6600'; - case 'green': - return '00ff00'; - case 'purple': - return '9900ff'; - case 'blue': - return '0000ff'; - case 'sky': - return '00ccff'; - case 'lime': - return '00ff99'; - case 'pink': - return 'ff66cc'; - case 'black': - return '000000'; - default: - return 'ffffff'; - } - } - - private function importBoard() { - $this->board = $this->boardService->create( - $this->data->name, - $this->settings->owner->getUID(), - $this->settings->color - ); - // $this->boardService->find($this->board->getId()); - } - - public function importLabels() { - $this->labels = []; - foreach ($this->data->labels as $label) { - if (empty($label->name)) { - $labelTitle = 'Unnamed ' . $label->color . ' label'; - } else { - $labelTitle = $label->name; - } - $newLabel = $this->labelService->create( - $labelTitle, - $this->translateColor($label->color), - $this->board->getId() - ); - $this->labels[$label->id] = $newLabel; - } - } - - private function setUserId() { - $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); - $propertyPermissionService->setAccessible(true); - $permissionService = $propertyPermissionService->getValue($this->labelService); - - $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); - $propertyUserId->setAccessible(true); - $propertyUserId->setValue($permissionService, $this->settings->owner->getUID()); + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->getSystem() + ->import($input, $output); + $output->writeln('Done!'); + return 0; } } diff --git a/lib/Command/Helper/ImportAbstract.php b/lib/Command/Helper/ImportAbstract.php new file mode 100644 index 000000000..de271f2c8 --- /dev/null +++ b/lib/Command/Helper/ImportAbstract.php @@ -0,0 +1,86 @@ +command = $command; + } + + /** + * @return Command + */ + public function getCommand() { + return $this->command; + } + + /** + * Get a setting + * + * @param string $setting Setting name + * @return mixed + */ + public function getSetting($setting) { + return $this->settings->$setting; + } + + /** + * Define a setting + * + * @param string $settingName + * @param mixed $value + * @return void + */ + public function setSetting($settingName, $value) { + $this->settings->$settingName = $value; + } + + protected function validateSettings(InputInterface $input, OutputInterface $output): void { + $settingFile = $input->getOption('setting'); + if (!is_file($settingFile)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid setting json file: ', + 'config.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Setting file not found' + ); + } + return $answer; + }); + $settingFile = $helper->ask($input, $output, $question); + $input->setOption('setting', $settingFile); + } + + $this->settings = json_decode(file_get_contents($settingFile)); + $validator = new Validator(); + $validator->validate( + $this->settings, + (object)['$ref' => 'file://' . realpath(__DIR__ . '/../fixtures/setting-schema.json')] + ); + if (!$validator->isValid()) { + $output->writeln('Invalid setting file'); + $output->writeln(array_map(function ($v) { + return $v['message']; + }, $validator->getErrors())); + $output->writeln('Valid schema:'); + $output->writeln(print_r(file_get_contents(__DIR__ . '/fixtures/setting-schema.json'), true)); + $input->setOption('setting', null); + $this->validateSettings($input, $output); + } + } +} diff --git a/lib/Command/Helper/ImportInterface.php b/lib/Command/Helper/ImportInterface.php new file mode 100644 index 000000000..08f0e6a68 --- /dev/null +++ b/lib/Command/Helper/ImportInterface.php @@ -0,0 +1,47 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command\Helper; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +interface ImportInterface { + /** + * Validate data before run execute method + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + public function validate(InputInterface $input, OutputInterface $output): void; + + /** + * Run import + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + */ + public function import(InputInterface $input, OutputInterface $output): void; +} diff --git a/lib/Command/Helper/TrelloHelper.php b/lib/Command/Helper/TrelloHelper.php new file mode 100644 index 000000000..50e1be346 --- /dev/null +++ b/lib/Command/Helper/TrelloHelper.php @@ -0,0 +1,429 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command\Helper; + +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Assignment; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\Label; +use OCA\Deck\Db\Stack; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\LabelService; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + +class TrelloHelper extends ImportAbstract implements ImportInterface { + /** @var BoardService */ + private $boardService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var AssignmentMapper */ + private $assignmentMapper; + /** @var AclMapper */ + private $aclMapper; + /** @var IDBConnection */ + private $connection; + /** @var IUserManager */ + private $userManager; + /** @var TrelloActions */ + private $trelloActions; + /** @var Board */ + private $board; + /** @var LabelService */ + private $labelService; + /** + * Data object created from JSON of origin system + * + * @var \StdClass + */ + private $data; + /** + * Array of stacks + * + * @var Stack[] + */ + private $stacks = []; + /** + * Array of labels + * + * @var Label[] + */ + private $labels = []; + /** @var Card[] */ + private $cards = []; + /** @var IUser */ + private $members = []; + + public function __construct( + BoardService $boardService, + LabelService $labelService, + StackMapper $stackMapper, + CardMapper $cardMapper, + AssignmentMapper $assignmentMapper, + AclMapper $aclMapper, + IDBConnection $connection, + IUserManager $userManager + ) { + $this->boardService = $boardService; + $this->labelService = $labelService; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + $this->assignmentMapper = $assignmentMapper; + $this->aclMapper = $aclMapper; + $this->connection = $connection; + $this->userManager = $userManager; + } + + public function validate(InputInterface $input, OutputInterface $output): void { + $this->validateData($input, $output); + $this->validateSettings($input, $output); + $this->validateUsers(); + $this->validateOwner(); + } + + public function import(InputInterface $input, OutputInterface $output): void { + $this->setUserId(); + $output->writeln('Importing board...'); + $this->importBoard(); + $output->writeln('Assign users to board...'); + $this->assignUsersToBoard(); + $output->writeln('Importing labels...'); + $this->importLabels(); + $output->writeln('Importing stacks...'); + $this->importStacks(); + $output->writeln('Importing cards...'); + $this->importCards(); + } + + private function assignUsersToBoard() { + foreach ($this->members as $member) { + $acl = new Acl(); + $acl->setBoardId($this->board->getId()); + $acl->setType(Acl::PERMISSION_TYPE_USER); + $acl->setParticipant($member->getUid()); + $acl->setPermissionEdit(true); + $acl->setPermissionShare($member->getUID() === $this->getSetting('owner')->getUID()); + $acl->setPermissionManage($member->getUID() === $this->getSetting('owner')->getUID()); + $this->aclMapper->insert($acl); + } + } + + private function validateData(InputInterface $input, OutputInterface $output): void { + $filename = $input->getOption('data'); + if (!is_file($filename)) { + $helper = $this->getCommand()->getHelper('question'); + $question = new Question( + 'Please inform a valid data json file: ', + 'data.json' + ); + $question->setValidator(function ($answer) { + if (!is_file($answer)) { + throw new \RuntimeException( + 'Data file not found' + ); + } + return $answer; + }); + $data = $helper->ask($input, $output, $question); + $input->setOption('data', $data); + } + $this->data = json_decode(file_get_contents($filename)); + if (!$this->data) { + $output->writeln('Is not a json file: ' . $filename . ''); + $this->validateData($input, $output); + } + } + + private function validateOwner(): void { + $owner = $this->userManager->get($this->getSetting('owner')); + if (!$owner) { + throw new \LogicException('Owner "' . $this->getSetting('owner') . '" not found on Nextcloud. Check setting json.'); + } + $this->setSetting('owner', $owner); + } + + /** + * @return void + */ + private function validateUsers() { + if (empty($this->getSetting('uidRelation'))) { + return; + } + foreach ($this->getSetting('uidRelation') as $trelloUid => $nextcloudUid) { + $user = array_filter($this->data->members, function ($u) use ($trelloUid) { + return $u->username === $trelloUid; + }); + if (!$user) { + throw new \LogicException('Trello user ' . $trelloUid . ' not found in property "members" of json data'); + } + if (!is_string($nextcloudUid)) { + throw new \LogicException('User on setting uidRelation must be a string'); + } + $this->getSetting('uidRelation')->$trelloUid = $this->userManager->get($nextcloudUid); + if (!$this->getSetting('uidRelation')->$trelloUid) { + throw new \LogicException('User on setting uidRelation not found: ' . $nextcloudUid); + } + $user = current($user); + $this->members[$user->id] = $this->getSetting('uidRelation')->$trelloUid; + } + } + + private function checklistItem($item): string { + if (($item->state == 'incomplete')) { + $string_start = '- [ ]'; + } else { + $string_start = '- [x]'; + } + $check_item_string = $string_start . ' ' . $item->name . "\n"; + return $check_item_string; + } + + private function formulateChecklistText($checklist): string { + $checklist_string = "\n\n## {$checklist->name}\n"; + foreach ($checklist->checkItems as $item) { + $checklist_item_string = $this->checklistItem($item); + $checklist_string = $checklist_string . "\n" . $checklist_item_string; + } + return $checklist_string; + } + + private function importCards(): void { + $checklists = []; + foreach ($this->data->checklists as $checklist) { + $checklists[$checklist->idCard][$checklist->id] = $this->formulateChecklistText($checklist); + } + $this->data->checklists = $checklists; + + foreach ($this->data->cards as $trelloCard) { + $card = new Card(); + $lastModified = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->dateLastActivity); + $card->setLastModified($lastModified->format('Y-m-d H:i:s')); + if ($trelloCard->closed) { + $card->setDeletedAt($lastModified->format('U')); + } + if ((count($trelloCard->idChecklists) !== 0)) { + foreach ($this->data->checklists[$trelloCard->id] as $checklist) { + $trelloCard->desc .= "\n" . $checklist; + } + } + $this->appendAttachmentsToDescription($trelloCard); + + $card->setTitle($trelloCard->name); + $card->setStackId($this->stacks[$trelloCard->idList]->getId()); + $card->setType('plain'); + $card->setOrder($trelloCard->idShort); + $card->setOwner($this->getSetting('owner')->getUID()); + $card->setDescription($trelloCard->desc); + if ($trelloCard->due) { + $duedate = \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloCard->due) + ->format('Y-m-d H:i:s'); + $card->setDuedate($duedate); + } + $card = $this->cardMapper->insert($card); + $this->cards[$trelloCard->id] = $card; + + $this->associateCardToLabels($card, $trelloCard); + $this->importComments($card, $trelloCard); + $this->assignToMember($card, $trelloCard); + } + } + + private function appendAttachmentsToDescription($trelloCard) { + if (empty($trelloCard->attachments)) { + return; + } + $translations = $this->getSetting('translations'); + $attachmentsLabel = empty($translations->{'Attachments'}) ? 'Attachments' : $translations->{'Attachments'}; + $URLLabel = empty($translations->{'URL'}) ? 'URL' : $translations->{'URL'}; + $nameLabel = empty($translations->{'Name'}) ? 'Name' : $translations->{'Name'}; + $dateLabel = empty($translations->{'Date'}) ? 'Date' : $translations->{'Date'}; + $trelloCard->desc .= "\n\n## {$attachmentsLabel}\n"; + $trelloCard->desc .= "| $URLLabel | $nameLabel | $dateLabel |\n"; + $trelloCard->desc .= "|---|---|---|\n"; + foreach ($trelloCard->attachments as $attachment) { + $name = $attachment->name === $attachment->url ? null : $attachment->name; + $trelloCard->desc .= "| {$attachment->url} | {$name} | {$attachment->date} |\n"; + } + } + + private function assignToMember(Card $card, $trelloCard) { + foreach ($trelloCard->idMembers as $idMember) { + $assignment = new Assignment(); + $assignment->setCardId($card->getId()); + $assignment->setParticipant($this->members[$idMember]->getUID()); + $assignment->setType(Assignment::TYPE_USER); + $assignment = $this->assignmentMapper->insert($assignment); + } + } + + private function importComments(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { + $comments = array_filter( + $this->data->actions, + function ($a) use ($trelloCard) { + return $a->type === 'commentCard' && $a->data->card->id === $trelloCard->id; + } + ); + foreach ($comments as $trelloComment) { + if (!empty($this->getSetting('uidRelation')->{$trelloComment->memberCreator->username})) { + $actor = $this->getSetting('uidRelation')->{$trelloComment->memberCreator->username}->getUID(); + } else { + $actor = $this->getSetting('owner')->getUID(); + } + $message = $this->replaceUsernames($trelloComment->data->text); + $qb = $this->connection->getQueryBuilder(); + + $values = [ + 'parent_id' => $qb->createNamedParameter(0), + 'topmost_parent_id' => $qb->createNamedParameter(0), + 'children_count' => $qb->createNamedParameter(0), + 'actor_type' => $qb->createNamedParameter('users'), + 'actor_id' => $qb->createNamedParameter($actor), + 'message' => $qb->createNamedParameter($message), + 'verb' => $qb->createNamedParameter('comment'), + 'creation_timestamp' => $qb->createNamedParameter( + \DateTime::createFromFormat('Y-m-d\TH:i:s.v\Z', $trelloComment->date) + ->format('Y-m-d H:i:s') + ), + 'latest_child_timestamp' => $qb->createNamedParameter(null), + 'object_type' => $qb->createNamedParameter('deckCard'), + 'object_id' => $qb->createNamedParameter($card->getId()), + ]; + + $qb->insert('comments') + ->values($values) + ->execute(); + } + } + + private function replaceUsernames($text) { + foreach ($this->getSetting('uidRelation') as $trello => $nextcloud) { + $text = str_replace($trello, $nextcloud->getUID(), $text); + } + return $text; + } + + private function associateCardToLabels(\OCP\AppFramework\Db\Entity $card, $trelloCard): void { + foreach ($trelloCard->labels as $label) { + $this->cardMapper->assignLabel( + $card->getId(), + $this->labels[$label->id]->getId() + ); + } + } + + private function importStacks(): void { + $this->stacks = []; + foreach ($this->data->lists as $order => $list) { + $stack = new Stack(); + if ($list->closed) { + $stack->setDeletedAt(time()); + } + $stack->setTitle($list->name); + $stack->setBoardId($this->board->getId()); + $stack->setOrder($order + 1); + $stack = $this->stackMapper->insert($stack); + $this->stacks[$list->id] = $stack; + } + } + + private function translateColor($color): string { + switch ($color) { + case 'red': + return 'ff0000'; + case 'yellow': + return 'ffff00'; + case 'orange': + return 'ff6600'; + case 'green': + return '00ff00'; + case 'purple': + return '9900ff'; + case 'blue': + return '0000ff'; + case 'sky': + return '00ccff'; + case 'lime': + return '00ff99'; + case 'pink': + return 'ff66cc'; + case 'black': + return '000000'; + default: + return 'ffffff'; + } + } + + private function importBoard(): void { + $this->board = $this->boardService->create( + $this->data->name, + $this->getSetting('owner')->getUID(), + $this->getSetting('color') + ); + } + + private function importLabels(): void { + $this->labels = []; + foreach ($this->data->labels as $label) { + if (empty($label->name)) { + $labelTitle = 'Unnamed ' . $label->color . ' label'; + } else { + $labelTitle = $label->name; + } + $newLabel = $this->labelService->create( + $labelTitle, + $this->translateColor($label->color), + $this->board->getId() + ); + $this->labels[$label->id] = $newLabel; + } + } + + private function setUserId(): void { + if (!property_exists($this->labelService, 'permissionService')) { + return; + } + $propertyPermissionService = new \ReflectionProperty($this->labelService, 'permissionService'); + $propertyPermissionService->setAccessible(true); + $permissionService = $propertyPermissionService->getValue($this->labelService); + + if (!property_exists($permissionService, 'userId')) { + return; + } + + $propertyUserId = new \ReflectionProperty($permissionService, 'userId'); + $propertyUserId->setAccessible(true); + $propertyUserId->setValue($permissionService, $this->getSetting('owner')->getUID()); + } +} diff --git a/lib/Command/UserExport.php b/lib/Command/UserExport.php index 1d73e8dcf..0b595c773 100644 --- a/lib/Command/UserExport.php +++ b/lib/Command/UserExport.php @@ -63,6 +63,9 @@ class UserExport extends Command { $this->groupManager = $groupManager; } + /** + * @return void + */ protected function configure() { $this ->setName('deck:export') diff --git a/lib/Command/fixtures/setting-schema.json b/lib/Command/fixtures/setting-schema.json index b21501c2b..f63cb3529 100644 --- a/lib/Command/fixtures/setting-schema.json +++ b/lib/Command/fixtures/setting-schema.json @@ -12,6 +12,9 @@ "type": "string", "required": true, "pattern": "^[0-9a-fA-F]{6}$" + }, + "translations": { + "type": "object" } } } \ No newline at end of file diff --git a/tests/unit/Command/BoardImportTest.php b/tests/unit/Command/BoardImportTest.php new file mode 100644 index 000000000..a6d9c904c --- /dev/null +++ b/tests/unit/Command/BoardImportTest.php @@ -0,0 +1,77 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command; + +use OCA\Deck\Command\Helper\TrelloHelper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class BoardImportTest extends \Test\TestCase { + /** @var TrelloHelper */ + private $trelloHelper; + /** @var BoardImport */ + private $boardImport; + + public function setUp(): void { + parent::setUp(); + $this->trelloHelper = $this->createMock(TrelloHelper::class); + $this->boardImport = new BoardImport( + $this->trelloHelper + ); + $questionHelper = new QuestionHelper(); + $this->boardImport->setHelperSet( + new HelperSet([ + $questionHelper + ]) + ); + } + + public function testExecuteWithSuccess() { + $input = $this->createMock(InputInterface::class); + + $input->method('getOption') + ->withConsecutive( + [$this->equalTo('system')], + [$this->equalTo('setting')], + [$this->equalTo('data')] + ) + ->will($this->returnValueMap([ + ['system', 'trello'], + ['setting', __DIR__ . '/fixtures/setting-trello.json'], + ['data', __DIR__ . '/fixtures/data-trello.json'] + ])); + $output = $this->createMock(OutputInterface::class); + + $output + ->expects($this->once()) + ->method('writeLn') + ->with('Done!'); + + $this->invokePrivate($this->boardImport, 'interact', [$input, $output]); + $actual = $this->invokePrivate($this->boardImport, 'execute', [$input, $output]); + $this->assertEquals(0, $actual); + } +} diff --git a/tests/unit/Command/Helper/TrelloHelperTest.php b/tests/unit/Command/Helper/TrelloHelperTest.php new file mode 100644 index 000000000..1e4dd6b8d --- /dev/null +++ b/tests/unit/Command/Helper/TrelloHelperTest.php @@ -0,0 +1,134 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Deck\Command; + +use OCA\Deck\Command\Helper\TrelloHelper; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\LabelService; +use OCP\IDBConnection; +use OCP\IUserManager; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class TrelloHelperTest extends \Test\TestCase { + /** @var BoardService */ + private $boardService; + /** @var LabelService */ + private $labelService; + /** @var StackMapper */ + private $stackMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var IDBConnection */ + private $connection; + /** @var IUserManager */ + private $userManager; + /** @var TrelloHelper */ + private $trelloHelper; + public function setUp(): void { + parent::setUp(); + $this->boardService = $this->createMock(BoardService::class); + $this->labelService = $this->createMock(LabelService::class); + $this->stackMapper = $this->createMock(StackMapper::class); + $this->cardMapper = $this->createMock(CardMapper::class); + $this->assignmentMapper = $this->createMock(AssignmentMapper::class); + $this->aclMapper = $this->createMock(AclMapper::class); + $this->connection = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->trelloHelper = new TrelloHelper( + $this->boardService, + $this->labelService, + $this->stackMapper, + $this->cardMapper, + $this->assignmentMapper, + $this->aclMapper, + $this->connection, + $this->userManager + ); + $questionHelper = new QuestionHelper(); + $command = new BoardImport($this->trelloHelper); + $command->setHelperSet( + new HelperSet([ + $questionHelper + ]) + ); + $this->trelloHelper->setCommand($command); + } + + public function testImportWithSuccess() { + $input = $this->createMock(InputInterface::class); + + $input->method('getOption') + ->withConsecutive( + [$this->equalTo('data')], + [$this->equalTo('setting')] + ) + ->will($this->returnValueMap([ + ['data', __DIR__ . '/../fixtures/data-trello.json'], + ['setting', __DIR__ . '/../fixtures/setting-trello.json'] + ])); + $output = $this->createMock(OutputInterface::class); + + $user = $this->createMock(\OCP\IUser::class); + $user + ->method('getUID') + ->willReturn('admin'); + $this->userManager + ->method('get') + ->willReturn($user); + $this->userManager + ->method('get') + ->willReturn($user); + $board = $this->createMock(\OCA\Deck\Db\Board::class); + $this->boardService + ->expects($this->once()) + ->method('create') + ->willReturn($board); + $label = $this->createMock(\OCA\Deck\Db\Label::class); + $this->labelService + ->expects($this->once()) + ->method('create') + ->willReturn($label); + $stack = $this->createMock(\OCA\Deck\Db\Stack::class); + $this->stackMapper + ->expects($this->once()) + ->method('insert') + ->willReturn($stack); + $card = $this->createMock(\OCA\Deck\Db\Card::class); + $this->cardMapper + ->expects($this->once()) + ->method('insert') + ->willReturn($card); + + $this->trelloHelper->validate($input, $output); + $actual = $this->trelloHelper->import($input, $output); + $this->assertNull($actual); + } +} diff --git a/tests/unit/Command/fixtures/data-trello.json b/tests/unit/Command/fixtures/data-trello.json new file mode 100644 index 000000000..5d27a8c54 --- /dev/null +++ b/tests/unit/Command/fixtures/data-trello.json @@ -0,0 +1,582 @@ +{ + "id": "fakeboardidhash", + "name": "Test Board Name", + "desc": "", + "descData": null, + "closed": false, + "dateClosed": null, + "idOrganization": null, + "shortLink": "qwerty", + "powerUps": [], + "dateLastActivity": "2021-07-10T17:01:58.633Z", + "idTags": [], + "datePluginDisable": null, + "creationMethod": null, + "idBoardSource": null, + "idMemberCreator": "fakeidmemberhash", + "idEnterprise": null, + "pinned": false, + "starred": false, + "url": "https://trello.com/b/qwerty/fakeboardurl", + "prefs": { + "permissionLevel": "private", + "hideVotes": false, + "voting": "disabled", + "comments": "members", + "invitations": "members", + "selfJoin": false, + "cardCovers": true, + "isTemplate": false, + "cardAging": "regular", + "calendarFeedEnabled": false, + "background": "blue", + "backgroundImage": null, + "backgroundImageScaled": null, + "backgroundTile": false, + "backgroundBrightness": "dark", + "backgroundColor": "#0079BF", + "backgroundBottomColor": "#0079BF", + "backgroundTopColor": "#0079BF", + "canBePublic": true, + "canBeEnterprise": true, + "canBeOrg": true, + "canBePrivate": true, + "canInvite": true + }, + "shortUrl": "https://trello.com/b/qwerty", + "premiumFeatures": [], + "enterpriseOwned": false, + "ixUpdate": "67", + "limits": { + "attachments": { + "perBoard": { + "status": "ok", + "disableAt": 36000, + "warnAt": 32400 + }, + "perCard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "boards": { + "totalMembersPerBoard": { + "status": "ok", + "disableAt": 1600, + "warnAt": 1440 + } + }, + "cards": { + "openPerBoard": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "openPerList": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "totalPerBoard": { + "status": "ok", + "disableAt": 2000000, + "warnAt": 1800000 + }, + "totalPerList": { + "status": "ok", + "disableAt": 1000000, + "warnAt": 900000 + } + }, + "checklists": { + "perBoard": { + "status": "ok", + "disableAt": 2000000, + "warnAt": 1800000 + }, + "perCard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + } + }, + "checkItems": { + "perChecklist": { + "status": "ok", + "disableAt": 200, + "warnAt": 180 + } + }, + "customFields": { + "perBoard": { + "status": "ok", + "disableAt": 50, + "warnAt": 45 + } + }, + "customFieldOptions": { + "perField": { + "status": "ok", + "disableAt": 50, + "warnAt": 45 + } + }, + "labels": { + "perBoard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "lists": { + "openPerBoard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + }, + "totalPerBoard": { + "status": "ok", + "disableAt": 3000, + "warnAt": 2700 + } + }, + "stickers": { + "perCard": { + "status": "ok", + "disableAt": 70, + "warnAt": 63 + } + }, + "reactions": { + "perAction": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + }, + "uniquePerAction": { + "status": "ok", + "disableAt": 17, + "warnAt": 16 + } + } + }, + "subscribed": false, + "templateGallery": null, + "dateLastView": "2021-07-10T17:01:58.665Z", + "labelNames": { + "green": "", + "yellow": "", + "orange": "", + "red": "", + "purple": "", + "blue": "", + "sky": "", + "lime": "", + "pink": "", + "black": "" + }, + "actions": [ + { + "id": "60e9d2869efe2e1141be2798", + "idMemberCreator": "fakeidmemberhash", + "data": { + "idMember": "fakeidmemberhash", + "deactivated": false, + "card": { + "id": "hashcard7", + "name": "Name Card 7", + "idShort": 7, + "shortLink": "fakeshortlinkcard7" + }, + "board": { + "id": "fakeboardidhash", + "name": "Test Board Name", + "shortLink": "qwerty" + }, + "member": { + "id": "fakeidmemberhash", + "name": "John Doe" + } + }, + "type": "removeMemberFromCard", + "date": "2021-07-10T17:01:58.636Z", + "appCreator": null, + "limits": {}, + "member": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + }, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "60e9d1832ff82d10c0cea6ba", + "idMemberCreator": "fakeidmemberhash", + "data": { + "idMember": "fakeidmemberhash", + "card": { + "id": "hashcard7", + "name": "Name Card 7", + "idShort": 7, + "shortLink": "fakeshortlinkcard7" + }, + "board": { + "id": "fakeboardidhash", + "name": "Test Board Name", + "shortLink": "qwerty" + }, + "member": { + "id": "fakeidmemberhash", + "name": "John Doe" + } + }, + "type": "addMemberToCard", + "date": "2021-07-10T16:57:39.999Z", + "appCreator": null, + "limits": {}, + "member": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + }, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfc4bf36aa0270d6bfd43", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + }, + "list": { + "name": "TODO", + "id": "hashlisttodo" + }, + "card": { + "shortLink": "fakeshortlinkcard7", + "idShort": 7, + "name": "Name Card 7", + "id": "hashcard7" + } + }, + "type": "createCard", + "date": "2017-09-15T16:14:03.187Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfb8e4a6f8ca35be9b82a", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + }, + "list": { + "name": "TODO", + "id": "hashlisttodo" + } + }, + "type": "createList", + "date": "2017-09-15T16:10:54.714Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + }, + { + "id": "59bbfb88973b76e586edec5e", + "idMemberCreator": "fakeidmemberhash", + "data": { + "board": { + "shortLink": "qwerty", + "name": "Test Board Name", + "id": "fakeboardidhash" + } + }, + "type": "createBoard", + "date": "2017-09-15T16:10:48.069Z", + "appCreator": null, + "limits": {}, + "memberCreator": { + "id": "fakeidmemberhash", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idMemberReferrer": null, + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true + } + } + ], + "cards": [ + { + "id": "hashcard7", + "address": null, + "checkItemStates": null, + "closed": false, + "coordinates": null, + "creationMethod": null, + "dateLastActivity": "2021-07-10T17:01:58.633Z", + "desc": "", + "descData": null, + "dueReminder": null, + "idBoard": "fakeboardidhash", + "idLabels": [], + "idList": "hashlisttodo", + "idMembersVoted": [], + "idShort": 7, + "idAttachmentCover": null, + "locationName": null, + "manualCoverAttachment": false, + "name": "Name Card 7", + "pos": 65535, + "shortLink": "fakeshortlinkcard7", + "isTemplate": false, + "cardRole": null, + "badges": { + "attachmentsByType": { + "trello": { + "board": 0, + "card": 0 + } + }, + "location": false, + "votes": 0, + "viewingMemberVoted": false, + "subscribed": false, + "fogbugz": "", + "checkItems": 0, + "checkItemsChecked": 0, + "checkItemsEarliestDue": null, + "comments": 0, + "attachments": 0, + "description": false, + "due": null, + "dueComplete": false, + "start": null + }, + "dueComplete": false, + "due": null, + "email": "johndoe+card7@boards.trello.com", + "idChecklists": [], + "idMembers": [], + "labels": [], + "limits": { + "attachments": { + "perCard": { + "status": "ok", + "disableAt": 1000, + "warnAt": 900 + } + }, + "checklists": { + "perCard": { + "status": "ok", + "disableAt": 500, + "warnAt": 450 + } + }, + "stickers": { + "perCard": { + "status": "ok", + "disableAt": 70, + "warnAt": 63 + } + } + }, + "shortUrl": "https://trello.com/c/fakeshortlinkcard7", + "start": null, + "subscribed": false, + "url": "https://trello.com/c/fakeshortlinkcard7/7-name-card-7", + "cover": { + "idAttachment": null, + "color": null, + "idUploadedBackground": null, + "size": "normal", + "brightness": "dark", + "idPlugin": null + }, + "attachments": [], + "pluginData": [], + "customFieldItems": [] + } + ], + "labels": [ + { + "id": "59bbfb881314a339999eb855", + "idBoard": "fakeboardidhash", + "name": "", + "color": "yellow" + } + ], + "lists": [ + { + "id": "hashlisttodo", + "name": "TODO", + "closed": false, + "pos": 65535, + "softLimit": null, + "creationMethod": null, + "idBoard": "fakeboardidhash", + "limits": { + "cards": { + "openPerList": { + "status": "ok", + "disableAt": 5000, + "warnAt": 4500 + }, + "totalPerList": { + "status": "ok", + "disableAt": 1000000, + "warnAt": 900000 + } + } + }, + "subscribed": false + } + ], + "members": [ + { + "id": "fakeidmemberhash", + "bio": "", + "bioData": { + "emoji": {} + }, + "confirmed": true, + "memberType": "normal", + "username": "johndoe", + "activityBlocked": false, + "avatarHash": "fakeavatarhash", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "fullName": "John Doe", + "idEnterprise": null, + "idEnterprisesDeactivated": [], + "idMemberReferrer": null, + "idPremOrgsAdmin": [], + "initials": "JD", + "nonPublic": { + "fullName": "John Doe", + "initials": "JD", + "avatarUrl": "https://trello-members.s3.amazonaws.com/fakeidmemberhash/fakeavatarhash", + "avatarHash": "fakeavatarhash" + }, + "nonPublicAvailable": true, + "products": [], + "url": "https://trello.com/johndoe", + "status": "disconnected" + } + ], + "checklists": [], + "customFields": [], + "memberships": [ + { + "id": "59bbfb88973b76e586edec5d", + "idMember": "fakeidmemberhash", + "memberType": "admin", + "unconfirmed": false, + "deactivated": false + } + ], + "pluginData": [] +} \ No newline at end of file diff --git a/tests/unit/Command/fixtures/setting-trello.json b/tests/unit/Command/fixtures/setting-trello.json new file mode 100644 index 000000000..544118212 --- /dev/null +++ b/tests/unit/Command/fixtures/setting-trello.json @@ -0,0 +1,7 @@ +{ + "owner": "admin", + "color": "0800fd", + "uidRelation": { + "johndoe": "admin" + } +} \ No newline at end of file