From e87c063076e6e00c3554c963ca60906d17de2e90 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Sun, 25 Jul 2021 00:15:50 -0300 Subject: [PATCH] Documentation, improvements on validation, refactor Validate get boad change pattern of api params Import only one board by api Populate data from api Update class diagram Update documentation Add return when success Sort comments Fix order of cards Instructions of attachments Signed-off-by: Vitor Mattos --- docs/User_documentation_en.md | 77 ++++- docs/implement-import.md | 2 +- docs/resources/BoardImport.svg | 286 ++++++++++-------- docs/resources/BoardImport.yuml | 15 +- lib/Service/ABoardImportService.php | 1 + lib/Service/BoardImportService.php | 2 +- lib/Service/BoardImportTrelloApiService.php | 164 ++++++++-- lib/Service/BoardImportTrelloJsonService.php | 15 +- .../fixtures/config-trelloApi-schema.json | 13 +- 9 files changed, 400 insertions(+), 175 deletions(-) diff --git a/docs/User_documentation_en.md b/docs/User_documentation_en.md index 25ea8f8ee..824c80c5c 100644 --- a/docs/User_documentation_en.md +++ b/docs/User_documentation_en.md @@ -9,11 +9,14 @@ Project management, time management or ideation, Deck makes it easier for you to ## Using Deck Overall, Deck is easy to use. You can create boards, add users, share the Deck, work collaboratively and in real time. -1. [Create my first board](#1-create-my-first-board) -2. [Create stacks and cards](#2-create-stacks-and-cards) -3. [Handle cards options](#3-handle-cards-options) -4. [Archive old tasks](#4-archive-old-tasks) -5. [Manage your board](#5-manage-your-board) +- 1. [Create my first board](#1-create-my-first-board) +- 2. [Create stacks and cards](#2-create-stacks-and-cards) +- 3. [Handle cards options](#3-handle-cards-options) +- 4. [Archive old tasks](#4-archive-old-tasks) +- 5. [Manage your board](#5-manage-your-board) +- 6. [Import boards](#6-import-boards) + - [Trello JSON](#trello-json) + - [Trello API](#trello-api) ### 1. Create my first board In this example, we're going to create a board and share it with an other nextcloud user. @@ -69,6 +72,70 @@ The **sharing tab** allows you to add users or even groups to your boards. **Deleted objects** allows you to return previously deleted stacks or cards. The **Timeline** allows you to see everything that happened in your boards. Everything! +### 6. Import boards + +Importing can be done using the API or the `occ` `deck:import` command. + +It is possible to import from the following sources: + +#### Trello JSON + +Steps: +* Create the data file + * Access Trello + * go to the board you want to export + * Follow the steps in [Trello documentation](https://help.trello.com/article/747-exporting-data-from-trello-1) and export as JSON +* Create the configuration file +* Execute the import informing the import file path, data file and source as `Trello JSON` + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + +**Limitations**: + +Importing from a JSON file imports up to 1000 actions. To find out how many actions the board to be imported has, identify how many actions the JSON has. + +#### Trello API + +Import using API is recommended for boards with more than 1000 actions. + +Trello makes it possible to attach links to a card. Deck does not have this feature. Attachments and attachment links are added in a markdown table at the end of the description for every imported card that has attachments in Trello. + +* Get the API Key and API Token [here](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/#authentication-and-authorization) +* Get the ID of the board you want to import by making a request to: +https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&fields=id,name + + This ID you will use in the configuration file in the `board` property +* Create the configuration file + +Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON` + +Example configuration file: +```json +{ + "owner": "admin", + "color": "0800fd", + "api": { + "key": "0cc175b9c0f1b6a831c399e269772661", + "token": "92eb5ffee6ae2fec3ad71c777531578f4a8a08f09d37b73795649038408b5f33" + }, + "board": "8277e0910d750195b4487976", + "uidRelation": { + "johndoe": "johndoe" + } +} +``` + ## Search Deck provides a global search either through the unified search in the Nextcloud header or with the inline search next to the board controls. diff --git a/docs/implement-import.md b/docs/implement-import.md index cca83357d..51972c56d 100644 --- a/docs/implement-import.md +++ b/docs/implement-import.md @@ -1,4 +1,4 @@ ## Implement import * Create a new class `lib/service/BoardImportService.php` where `` is the name of the source system. -* Use the `lib/service/BoardImportTrelloService.php` class as inspiration \ No newline at end of file +* Use the `lib/service/BoardImportTrelloJsonService.php` class as inspiration \ No newline at end of file diff --git a/docs/resources/BoardImport.svg b/docs/resources/BoardImport.svg index 700538bdb..1d17034a8 100644 --- a/docs/resources/BoardImport.svg +++ b/docs/resources/BoardImport.svg @@ -4,175 +4,211 @@ - - + + G - + A0 - - - -Classes used on -board import. -Methods just to -illustrate. + + + +Classes used on +board import. +Methods just to +illustrate. A1 - -ApiController + +ApiController A2 - -BoardImportApiController - -+import() -+getAllowedSystems() -+getConfigSchema() + +BoardImportApiController + ++import() ++getAllowedSystems() ++getConfigSchema() A1->A2 - - + + A3 - -BoardImportService - -+import() -+bootstrap() -+validateSystem() -#validateConfig() -#validateData() + +BoardImportService + ++import() ++bootstrap() ++validateSystem() +#validateConfig() +#validateData() A2->A3 - - -uses + + +uses A7 - -BoardImportTrelloService + +BoardImportTrelloApiService + ++name:string A3->A7 - - -uses - - - -A8 - - - -validateSystem is -public because is -used on Api. - - - -A3->A8 - - - - -A4 - -Command - - - -A5 - -BoardImport - -+boardImportCommandService - -#configure() -#execute(input,output) - - - -A4->A5 - - - - - -A6 - -BoardImportCommandService - -+bootstrap() -+import() -+validateSystem() -#validateConfig() -#validateData() - - - -A5->A6 - - -uses - - - -A6->A3 - - - - - -A7->A3 - - -uses + + +uses A9 - - - -To create an import -to another system, -create another class -similar to this. + +BoardImportTrelloJsonService + ++name:string +#needValidateData:true - + -A7->A9 - +A3->A9 + + +uses A10 - -<<abstract>> -ABoardImportService + + + +validateSystem is +public because is +used on Api. - + + +A3->A10 + + + + +A4 + +Command + + + +A5 + +BoardImport + ++boardImportCommandService + +#configure() +#execute(input,output) + + + +A4->A5 + + + + + +A6 + +BoardImportCommandService + ++bootstrap() ++import() ++validateSystem() +#validateConfig() +#validateData() + + + +A5->A6 + + +uses + + + +A6->A3 + + + + + +A7->A3 + + +uses + + + +A8 + +<<abstract>> +ABoardImportService + +#needValidateData:false + ++needValidateData():bool + + + +A7->A8 + + +implements + + -A7->A10 - - -implements +A9->A3 + + +uses + + + +A9->A8 + + +implements + + + +A11 + + + +To create an import +to another system, +create another class +similar to this. + + + +A9->A11 + diff --git a/docs/resources/BoardImport.yuml b/docs/resources/BoardImport.yuml index 69aa222da..cbe89c829 100644 --- a/docs/resources/BoardImport.yuml +++ b/docs/resources/BoardImport.yuml @@ -5,13 +5,20 @@ // {generate:true} [note: Classes used on board import. Methods just to illustrate. {bg:cornsilk}] + [ApiController]<-[BoardImportApiController|+import();+getAllowedSystems();+getConfigSchema()] [BoardImportApiController]uses-.->[BoardImportService|+import();+bootstrap();+validateSystem();#validateConfig();#validateData();] + [Command]<-[BoardImport|+boardImportCommandService|#configure();#execute(input,output)] [BoardImport]uses-.->[BoardImportCommandService|+bootstrap();+import();+validateSystem();#validateConfig();#validateData()] [BoardImportCommandService]->[BoardImportService] -[BoardImportService]uses-.->[BoardImportTrelloService] -[BoardImportTrelloService]uses-.->[BoardImportService] + +[BoardImportService]uses-.->[BoardImportTrelloApiService|+name:string] +[BoardImportTrelloApiService]uses-.->[BoardImportService] +[BoardImportTrelloApiService]implements-.-^[<> ABoardImportService|#needValidateData:false|+needValidateData():bool] + +[BoardImportService]uses-.->[BoardImportTrelloJsonService|+name:string;#needValidateData:true] +[BoardImportTrelloJsonService]uses-.->[BoardImportService] [BoardImportService]-[note: validateSystem is public because is used on Api. {bg:cornsilk}] -[BoardImportTrelloService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] -[BoardImportTrelloService]implements-.-^[<> ABoardImportService] \ No newline at end of file +[BoardImportTrelloJsonService]-[note: To create an import to another system, create another class similar to this. {bg:cornsilk}] +[BoardImportTrelloJsonService]implements-.-^[<> ABoardImportService] diff --git a/lib/Service/ABoardImportService.php b/lib/Service/ABoardImportService.php index 746febbc9..cbadca6b6 100644 --- a/lib/Service/ABoardImportService.php +++ b/lib/Service/ABoardImportService.php @@ -37,6 +37,7 @@ abstract class ABoardImportService { public static $name = ''; /** @var BoardImportService */ private $boardImportService; + /** @var bool */ protected $needValidateData = true; /** @var Stack[] */ protected $stacks = []; diff --git a/lib/Service/BoardImportService.php b/lib/Service/BoardImportService.php index 3a35a57aa..07cfb790d 100644 --- a/lib/Service/BoardImportService.php +++ b/lib/Service/BoardImportService.php @@ -180,7 +180,7 @@ class BoardImportService { $className = 'OCA\Deck\Service\\'.$matches['class']; if (!class_exists($className)) { /** @psalm-suppress UnresolvableInclude */ - require_once $name; + require_once $className; } /** @psalm-suppress InvalidPropertyFetch */ $name = $className::$name; diff --git a/lib/Service/BoardImportTrelloApiService.php b/lib/Service/BoardImportTrelloApiService.php index 5a8c54117..94e8f2432 100644 --- a/lib/Service/BoardImportTrelloApiService.php +++ b/lib/Service/BoardImportTrelloApiService.php @@ -23,14 +23,11 @@ namespace OCA\Deck\Service; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\RequestException; -use OCP\AppFramework\Http; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IL10N; -use OCP\ILogger; use OCP\IUserManager; +use Psr\Log\LoggerInterface; class BoardImportTrelloApiService extends BoardImportTrelloJsonService { /** @var string */ @@ -38,16 +35,17 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { protected $needValidateData = false; /** @var IClient */ private $httpClient; - /** @var ILogger */ + /** @var LoggerInterface */ protected $logger; /** @var string */ private $baseApiUrl = 'https://api.trello.com/1'; - + /** @var ?\stdClass[] */ + private $boards; public function __construct( IUserManager $userManager, IL10N $l10n, - ILogger $logger, + LoggerInterface $logger, IClientService $httpClientService ) { parent::__construct($userManager, $l10n); @@ -56,44 +54,150 @@ class BoardImportTrelloApiService extends BoardImportTrelloJsonService { } public function bootstrap(): void { - $this->getBoards(); + $this->populateBoard(); + $this->populateMembers(); + $this->populateLabels(); + $this->populateLists(); + $this->populateCheckLists(); + $this->populateCards(); + $this->populateActions(); parent::bootstrap(); } - private function getBoards() { - $boards = $this->doRequest('/members/me/boards'); + private function populateActions(): void { + $data = $this->getImportService()->getData(); + $data->actions = $this->doRequest( + '/boards/' . $data->id . '/actions', + [ + 'filter' => 'commentCard', + 'fields=memberCreator,type,data,date', + 'memberCreator_fields' => 'username', + 'limit' => 1000 + ] + ); } - private function doRequest($path, $queryString = []) { + private function populateCards(): void { + $data = $this->getImportService()->getData(); + $data->cards = $this->doRequest( + '/boards/' . $data->id . '/cards', + [ + 'fields' => 'id,idMembers,dateLastActivity,closed,idChecklists,name,idList,pos,desc,due,labels', + 'attachments' => true, + 'attachment_fields' => 'name,url,date', + 'limit' => 1000 + ] + ); + } + + private function populateCheckLists(): void { + $data = $this->getImportService()->getData(); + $data->checklists = $this->doRequest( + '/boards/' . $data->id . '/checkLists', + [ + 'fields' => 'id,idCard,name', + 'checkItem_fields' => 'id,state,name', + 'limit' => 1000 + ] + ); + } + + private function populateLists(): void { + $data = $this->getImportService()->getData(); + $data->lists = $this->doRequest( + '/boards/' . $data->id . '/lists', + [ + 'fields' => 'id,name,closed', + 'limit' => 1000 + ] + ); + } + + private function populateLabels(): void { + $data = $this->getImportService()->getData(); + $data->labels = $this->doRequest( + '/boards/' . $data->id . '/labels', + [ + 'fields' => 'id,color,name', + 'limit' => 1000 + ] + ); + } + + private function populateMembers(): void { + $data = $this->getImportService()->getData(); + $data->members = $this->doRequest( + '/boards/' . $data->id . '/members', + [ + 'fields' => 'username', + 'limit' => 1000 + ] + ); + } + + private function populateBoard(): void { + $toImport = $this->getImportService()->getConfig('board'); + $board = $this->doRequest( + '/boards/' . $toImport, + ['fields' => 'id,name'] + ); + if ($board instanceof \stdClass) { + $this->getImportService()->setData($board); + return; + } + throw new \Exception('Invalid board id to import'); + } + + /** + * @return array|\stdClass + */ + private function doRequest(string $path = '', array $queryString = []) { + $target = $this->baseApiUrl . $path; try { - $target = $this->baseApiUrl . $path; $result = $this->httpClient ->get($target, $this->getQueryString($queryString)) ->getBody(); - $data = json_decode($result); - } catch (ClientException $e) { - $status = $e->getCode(); - if ($status === Http::STATUS_FORBIDDEN) { - $this->logger->info($target . ' refused.', ['app' => 'deck']); - } else { - $this->logger->info($target . ' responded with a ' . $status . ' containing: ' . $e->getMessage(), ['app' => 'deck']); + if (is_string($result)) { + $data = json_decode($result); + if (is_array($data)) { + $data = array_merge( + $data, + $this->paginate($path, $queryString, $data) + ); + } + return $data; } - } catch (RequestException $e) { - $this->logger->logException($e, [ - 'message' => 'Could not connect to ' . $target, - 'level' => ILogger::INFO, - 'app' => 'deck', - ]); + throw new \Exception('Invalid return of api'); } catch (\Throwable $e) { - $this->logger->logException($e, ['app' => 'deck']); + $this->logger->critical( + $e->getMessage(), + ['app' => 'deck'] + ); + throw new \Exception($e->getMessage()); } - return $data; } - private function getQueryString($params = []): array { + private function paginate(string $path = '', array $queryString = [], array $data = []): array { + if (empty($queryString['limit'])) { + return []; + } + if (count($data) < $queryString['limit']) { + return []; + } + $queryString['before'] = end($data)->id; + $return = $this->doRequest($path, $queryString); + if (is_array($return)) { + return $return; + } + throw new \Exception('Invalid return of api'); + } + + private function getQueryString(array $params = []): array { $apiSettings = $this->getImportService()->getConfig('api'); $params['key'] = $apiSettings->key; - $params['value'] = $apiSettings->token; - return $params; + $params['token'] = $apiSettings->token; + return [ + 'query' => $params + ]; } } diff --git a/lib/Service/BoardImportTrelloJsonService.php b/lib/Service/BoardImportTrelloJsonService.php index 9bd7f6ff4..5ea3fa429 100644 --- a/lib/Service/BoardImportTrelloJsonService.php +++ b/lib/Service/BoardImportTrelloJsonService.php @@ -111,6 +111,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $c->id; }, $values); $trelloComments = array_combine($keys, $values); + $trelloComments = $this->sortComments($trelloComments); foreach ($trelloComments as $commentId => $trelloComment) { $comment = new Comment(); if (!empty($this->getImportService()->getConfig('uidRelation')->{$trelloComment->memberCreator->username})) { @@ -131,6 +132,18 @@ class BoardImportTrelloJsonService extends ABoardImportService { return $comments; } + private function sortComments(array $comments): array { + $comparison = function($a, $b) { + if ($a->date == $b->date) { + return 0; + } + return ($a->date < $b->date) ? -1 : 1; + }; + + usort($comments, $comparison); + return $comments; + } + public function getCardLabelAssignment(): array { $cardsLabels = []; foreach ($this->getImportService()->getData()->cards as $trelloCard) { @@ -221,7 +234,7 @@ class BoardImportTrelloJsonService extends ABoardImportService { $cardsOnStack[] = $card; $this->stacks[$trelloCard->idList]->setCards($cardsOnStack); $card->setType('plain'); - $card->setOrder($trelloCard->idShort); + $card->setOrder($trelloCard->pos); $card->setOwner($this->getImportService()->getConfig('owner')->getUID()); $card->setDescription($trelloCard->desc); if ($trelloCard->due) { diff --git a/lib/Service/fixtures/config-trelloApi-schema.json b/lib/Service/fixtures/config-trelloApi-schema.json index baef76ce5..056c9e519 100644 --- a/lib/Service/fixtures/config-trelloApi-schema.json +++ b/lib/Service/fixtures/config-trelloApi-schema.json @@ -6,20 +6,17 @@ "properties": { "key": { "type": "string", - "pattern": "^\\w{32}$" + "pattern": "^[0-9a-fA-F]{32}$" }, "token": { "type": "string", - "pattern": "^\\w{1,}$" + "pattern": "^[0-9a-fA-F]{64}$" } } }, - "boards": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\w{1,}$" - } + "board": { + "type": "string", + "pattern": "^\\w{1,}$" }, "uidRelation": { "type": "object",