Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transfer ownership #2496

Merged
merged 29 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5d0b8d8
Add deck:transfer-ownership command
matchish May 21, 2020
19a2aeb
Update docs
matchish May 25, 2020
3d269e2
Add tests
matchish Jun 7, 2020
b45c454
Fix code style
matchish Jun 7, 2020
e3750a7
Fix wrong class name
matchish Jun 8, 2020
8b45495
Check type before transfer card participants ownership
matchish Jun 9, 2020
7df4b7c
Transfer deck ownership even if target user already participant of a …
matchish Jul 18, 2020
6106066
Fix coding styles
matchish Jul 18, 2020
ba7cadf
Fix card mapper query for transfer
juliusknorr Nov 3, 2020
3e7d0d3
Use proper description of what gets transferred
juliusknorr Nov 10, 2020
fa7fcef
Just cleanup old ACL rules, there are none for the board owner so not…
juliusknorr Nov 10, 2020
e8ada52
Make queries work with the new base mapper
juliusknorr Nov 10, 2020
4d3dabb
fix: Assignment is the new AssignedUsers
max-nextcloud Feb 9, 2022
b6340e5
fix: queries with the new base mapper in BoardMapper
max-nextcloud Feb 9, 2022
afbbdf0
fix: unit test & psalm static code analysis issues
luka-nextcloud Mar 4, 2022
4615926
fix: integration tests
luka-nextcloud Mar 8, 2022
e4551bd
feat: add integration test for transferring board ownership with data
luka-nextcloud Mar 10, 2022
72134e6
fix: unit tests
juliusknorr Mar 14, 2022
b774090
fix: Psalm
juliusknorr Mar 14, 2022
a45e46f
Allow transfer of single boards
juliusknorr Mar 14, 2022
a032287
cleanup test cases
juliusknorr Mar 14, 2022
c214437
fix: Properly handle limited scope for remapping users
juliusknorr Mar 14, 2022
4f13977
Reuse single board transfer for all user boards
juliusknorr Mar 14, 2022
3a4ec07
fix: test cases using generator
juliusknorr Mar 14, 2022
bf9a51d
feat: add api endpoint and UI to transfer a board to a different user
luka-nextcloud Mar 14, 2022
9f1dbd1
fix: feedback
luka-nextcloud Mar 16, 2022
3f29cd9
Cover case where the owner is preserved
juliusknorr Mar 21, 2022
23f0b16
Handle board exceptions more gracefully
juliusknorr Mar 22, 2022
c6aef45
Adjust documentaion wording
juliusknorr Mar 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ ifeq (, $(shell which phpunit 2> /dev/null))
php $(build_tools_directory)/phpunit.phar -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
php $(build_tools_directory)/phpunit.phar -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
else
phpunit -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
phpunit -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
phpunit -c tests/phpunit.integration.xml --testsuite=integration-database --coverage-clover build/php-integration.coverage.xml
endif

test-integration:
Expand Down
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
</commands>
<activity>
<settings>
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl/{aclId}', 'verb' => 'PUT'],
['name' => 'board#deleteAcl', 'url' => '/boards/{boardId}/acl/{aclId}', 'verb' => 'DELETE'],
['name' => 'board#clone', 'url' => '/boards/{boardId}/clone', 'verb' => 'POST'],
['name' => 'board#transferOwner', 'url' => '/boards/{boardId}/transferOwner', 'verb' => 'PUT'],

// stacks
['name' => 'stack#index', 'url' => '/stacks/{boardId}', 'verb' => 'GET'],
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^21@dev",
"christophwurst/nextcloud": "dev-master",
juliusknorr marked this conversation as resolved.
Show resolved Hide resolved
"phpunit/phpunit": "^9",
"nextcloud/coding-standard": "^1.0.0",
"symfony/event-dispatcher": "^4.0",
Expand Down
72 changes: 63 additions & 9 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion docs/User_documentation_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Overall, Deck is easy to use. You can create boards, add users, share the Deck,
5. [Manage your board](#5-manage-your-board)
6. [Import boards](#6-import-boards)
7. [Search](#7-search)
8. [New owner for the deck entities](#8-new-owner-for-the-deck-entities)

### 1. Create my first board
In this example, we're going to create a board and share it with an other nextcloud user.
Expand Down Expand Up @@ -158,4 +159,22 @@ For example the search `project tag:ToDo assigned:alice assigned:bob` will retur

Other text tokens will be used to perform a case-insensitive search on the card title and description

In addition wuotes can be used to pass a query with spaces, e.g. `"Exact match with spaces"` or `title:"My card"`.
In addition, quotes can be used to pass a query with spaces, e.g. `"Exact match with spaces"` or `title:"My card"`.

### 8. New owner for the deck entities
You can transfer ownership of boards, cards, etc to a new user, using `occ` command `deck:transfer-ownership`

```bash
php occ deck:transfer-ownership previousOwner newOwner
```

The transfer will preserve card details linked to the old owner, which can also be remapped by using the `--remap` option on the occ command.
```bash
php occ deck:transfer-ownership --remap previousOwner newOwner
```

Individual boards can be transferred by adding the id of the board to the command:

```bash
php occ deck:transfer-ownership previousOwner newOwner 123
```
105 changes: 105 additions & 0 deletions lib/Command/TransferOwnership.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace OCA\Deck\Command;

use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\PermissionService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
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\ConfirmationQuestion;

final class TransferOwnership extends Command {
protected $boardService;
protected $boardMapper;
protected $permissionService;
protected $questionHelper;

public function __construct(BoardService $boardService, BoardMapper $boardMapper, PermissionService $permissionService, QuestionHelper $questionHelper) {
parent::__construct();

$this->boardService = $boardService;
$this->boardMapper = $boardMapper;
$this->permissionService = $permissionService;
$this->questionHelper = $questionHelper;
}

protected function configure() {
$this
->setName('deck:transfer-ownership')
->setDescription('Change owner of deck boards')
->addArgument(
'owner',
InputArgument::REQUIRED,
'Owner uid'
)
->addArgument(
'newOwner',
InputArgument::REQUIRED,
'New owner uid'
)
->addArgument(
'boardId',
InputArgument::OPTIONAL,
'Single board ID'
)
->addOption(
'remap',
'r',
InputOption::VALUE_NONE,
'Reassign card details of the old owner to the new one'
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$owner = $input->getArgument('owner');
$newOwner = $input->getArgument('newOwner');
$boardId = $input->getArgument('boardId');

$remapAssignment = $input->getOption('remap');

$this->boardService->setUserId($owner);
$this->permissionService->setUserId($owner);

try {
$board = $boardId ? $this->boardMapper->find($boardId) : null;
} catch (\Exception $e) {
$output->writeln("Could not find a board for the provided id.");
return 1;
}

if ($boardId !== null && $board->getOwner() !== $owner) {
$output->writeln("$owner is not the owner of the board $boardId (" . $board->getTitle() . ")");
return 1;
}

if ($boardId) {
$output->writeln("Transfer board " . $board->getTitle() . " from ". $board->getOwner() ." to $newOwner");
} else {
$output->writeln("Transfer all boards from $owner to $newOwner");
}

$question = new ConfirmationQuestion('Do you really want to continue? (y/n) ', false);
if (!$this->questionHelper->ask($input, $output, $question)) {
return 1;
}

if ($boardId) {
$this->boardService->transferBoardOwnership($boardId, $newOwner, $remapAssignment);
$output->writeln("<info>Board " . $board->getTitle() . " from ". $board->getOwner() ." transferred to $newOwner completed</info>");
return 0;
}

foreach ($this->boardService->transferOwnership($owner, $newOwner, $remapAssignment) as $board) {
$output->writeln(" - " . $board->getTitle() . " transferred");
}
$output->writeln("<info>All boards from $owner to $newOwner transferred</info>");

return 0;
}
}
16 changes: 15 additions & 1 deletion lib/Controller/BoardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@
namespace OCA\Deck\Controller;

use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Board;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;

class BoardController extends ApiController {
Expand Down Expand Up @@ -150,9 +153,20 @@ public function deleteAcl($aclId) {
/**
* @NoAdminRequired
* @param $boardId
* @return \OCP\Deck\DB\Board
* @return Board
*/
public function clone($boardId) {
return $this->boardService->clone($boardId, $this->userId);
}

/**
* @NoAdminRequired
*/
public function transferOwner(int $boardId, string $newOwner): DataResponse {
if ($this->permissionService->userIsBoardOwner($boardId, $this->userId)) {
return new DataResponse($this->boardService->transferBoardOwnership($boardId, $newOwner), HTTP::STATUS_OK);
}

return new DataResponse([], HTTP::STATUS_UNAUTHORIZED);
}
}
13 changes: 13 additions & 0 deletions lib/Db/AclMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

class AclMapper extends DeckMapper implements IPermissionMapper {
Expand Down Expand Up @@ -57,4 +58,16 @@ public function findByParticipant($type, $participant): array {
$sql = 'SELECT * from *PREFIX*deck_board_acl WHERE type = ? AND participant = ?';
return $this->findEntities($sql, [$type, $participant]);
}

/**
* @throws \OCP\DB\Exception
*/
public function deleteParticipantFromBoard(int $boardId, int $type, string $participant): void {
$qb = $this->db->getQueryBuilder();
$qb->delete('deck_board_acl')
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)));
$qb->executeStatement();
}
}
36 changes: 36 additions & 0 deletions lib/Db/AssignmentMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use OCA\Deck\Service\CirclesService;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
Expand Down Expand Up @@ -146,4 +147,39 @@ private function getOrigin(Assignment $assignment) {
}
return null;
}

public function remapAssignedUser(int $boardId, string $userId, string $newUserId): void {
$subQuery = $this->db->getQueryBuilder();
$subQuery->selectAlias('a.id', 'id')
->from('deck_assigned_users', 'a')
->innerJoin('a', 'deck_cards', 'c', 'c.id = a.card_id')
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
->where($subQuery->expr()->eq('a.type', $subQuery->createNamedParameter(Assignment::TYPE_USER, IQueryBuilder::PARAM_INT)))
->andWhere($subQuery->expr()->eq('a.participant', $subQuery->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
->andWhere($subQuery->expr()->eq('s.board_id', $subQuery->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->setMaxResults(1000);

$qb = $this->db->getQueryBuilder();
juliusknorr marked this conversation as resolved.
Show resolved Hide resolved
$qb->update('deck_assigned_users')
->set('participant', $qb->createParameter('participant'))
->where($qb->expr()->in('id', $qb->createParameter('ids')));

$moreResults = true;
do {
$result = $subQuery->executeQuery();
$ids = array_map(function ($item) {
return $item['id'];
}, $result->fetchAll());

if (count($ids) === 0 || $result->rowCount() === 0) {
$moreResults = false;
}

$qb->setParameter('participant', $newUserId, IQueryBuilder::PARAM_STR);
$qb->setParameter('ids', $ids, IQueryBuilder::PARAM_INT_ARRAY);
$qb->executeStatement();
} while ($moreResults === true);

$result->closeCursor();
}
}
Loading