diff --git a/.drone.yml b/.drone.yml index d57d9fc1f..32e80c476 100644 --- a/.drone.yml +++ b/.drone.yml @@ -67,7 +67,7 @@ pipeline: matrix: TESTS: syntax-php7.0 php5.6: - image: nextcloudci/php5.6:php5.6-3 + image: nextcloudci/php5.6:php5.6-7 environment: - APP_NAME=deck - CORE_BRANCH=master @@ -93,7 +93,7 @@ pipeline: matrix: TESTS: php5.6 php7.0: - image: nextcloudci/php7.0:php7.0-7 + image: nextcloudci/php7.0:php7.0-8 environment: - APP_NAME=deck - CORE_BRANCH=master @@ -170,4 +170,4 @@ matrix: - TESTS: jsbuild - TESTS: integration -branches: [ master, stable* ] + diff --git a/appinfo/database.xml b/appinfo/database.xml index 50af46975..a49526952 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -38,6 +38,14 @@ boolean false + + deleted_at + integer + 0 + 8 + false + true + diff --git a/appinfo/info.xml b/appinfo/info.xml index 05f092643..6db4b30dd 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ 💥 This is still alpha software: it may not be stable enough for production! - 0.1.4.1 + 0.1.4.2agplJulius HärtlDeck @@ -30,6 +30,9 @@ + + OCA\Deck\Cron\DeleteCron + OCA\Deck\Migration\UnknownUsers diff --git a/appinfo/routes.php b/appinfo/routes.php index 52ddf0455..2f0e32af5 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -31,6 +31,7 @@ return [ ['name' => 'board#read', 'url' => '/boards/{boardId}', 'verb' => 'GET'], ['name' => 'board#update', 'url' => '/boards/{boardId}', 'verb' => 'PUT'], ['name' => 'board#delete', 'url' => '/boards/{boardId}', 'verb' => 'DELETE'], + ['name' => 'board#deleteUndo', 'url' => '/boards/{boardId}/deleteUndo', 'verb' => 'POST'], ['name' => 'board#getUserPermissions', 'url' => '/boards/{boardId}/permissions', 'verb' => 'GET'], ['name' => 'board#addAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'POST'], ['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'PUT'], diff --git a/css/style.css b/css/style.css index ef639ac2f..faeab3f5c 100644 --- a/css/style.css +++ b/css/style.css @@ -65,6 +65,14 @@ button.button-inline:hover { * Navigation sidebar */ +.app-navigation-entry-menu ul { + flex-direction: row; +} + +.app-navigation-entry-utils-menu-button { + display: block !important; +} + .app-navigation-entry-utils-menu-share { display: flex !important; padding: 14px; @@ -318,9 +326,9 @@ button.button-inline:hover { .popovermenu { z-index: 999; opacity: 1; - margin-top: 25px; - margin-right: 3px; display: block; + margin-top: 25px; + margin-right: 0px; } .popovermenu.hidden { @@ -614,23 +622,30 @@ button.button-inline:hover { .colorselect { overflow: hidden; - clear: both; - padding-top: 4px; - padding-left: 4px; + border-radius:3px; + flex-direction: row; + min-width: 240px; + height: 34px; + display: flex; + margin: 3px 3px 3px 0; } .colorselect .color { opacity: 0.7; - width: 27px; - height: 27px; - float: left; - margin-right: 2px; + height: 100%; + flex-grow: 1; border: none; } .colorselect .selected { + background-image: url(../../../core/img/actions/checkmark.svg); + background-position: center center; + background-repeat: no-repeat; opacity: 1.0; - border: 1px solid #333333; +} + +.colorselect .dark.selected { + background-image: url(../../../core/img/actions/checkmark-white.svg); } .labels .colorselect { @@ -662,6 +677,16 @@ button.button-inline:hover { cursor: pointer; display: block; } +#boardlist .app-popover-menu-utils { + width: 30px; + display: inline; + position: relative; +} + +.popovermenu ul { + display: flex !important; + flex-direction: column; +} #boardlist td { padding: 10px; @@ -680,15 +705,34 @@ button.button-inline:hover { .cell-board-title { width: 50%; - } -#boardlist .colorselect, -#boardlist input { - float: left; +#boardlist tr.deleted td * { + opacity: 0.5; } -#boardlist .colorselect { +#boardlist td form { + display: flex; + width: 100%; +} + +#boardlist td .colorselect { + flex-grow: 1; +} + +#boardlist td input[type=text] { + flex-grow: 2; +} + +#boardlist td input[type=submit] { + width: 32px; +} + +#boardlist td .app-popover-menu-utils { + float: right; +} + +#boardlist .popovermenu { margin-top: 5px; } @@ -925,6 +969,10 @@ button.button-inline:hover { * Custom icons */ +.icon-deck { + background-image: url(../img/deck.svg); +} + .icon-group { background-image: url('../../../settings/img/users.svg'); } diff --git a/img/deck.svg b/img/deck.svg new file mode 100644 index 000000000..9431bc06b --- /dev/null +++ b/img/deck.svg @@ -0,0 +1 @@ + diff --git a/js/app/Config.js b/js/app/Config.js index 3c3ae25b3..f1330ddb3 100644 --- a/js/app/Config.js +++ b/js/app/Config.js @@ -33,36 +33,40 @@ app.config(function ($provide, $routeProvider, $interpolateProvider, $httpProvid }); markdownItConverterProvider.use(markdownitLinkTarget); - $urlRouterProvider.otherwise("/"); + $urlRouterProvider.otherwise('/'); $stateProvider .state('list', { - url: "/", - templateUrl: "/boardlist.mainView.html", - controller: 'ListController' + url: '/:filter', + templateUrl: '/boardlist.mainView.html', + controller: 'ListController', + reloadOnSearch: false, + params: { + filter: { value: '', dynamic: true } + } }) .state('board', { - url: "/board/:boardId/:filter", - templateUrl: "/board.html", + url: '/board/:boardId/:filter', + templateUrl: '/board.html', controller: 'BoardController', params: { filter: { value: '', dynamic: true } } }) .state('board.detail', { - url: "/detail/", + url: '/detail/', reloadOnSearch : false, views: { - "sidebarView": { - templateUrl: "/board.sidebarView.html" + 'sidebarView': { + templateUrl: '/board.sidebarView.html' } } }) .state('board.card', { - url: "/card/:cardId", + url: '/card/:cardId', views: { - "sidebarView": { - templateUrl: "/card.sidebarView.html", + 'sidebarView': { + templateUrl: '/card.sidebarView.html', controller: 'CardController' } } diff --git a/js/app/Run.js b/js/app/Run.js index e76787b11..274f689a2 100644 --- a/js/app/Run.js +++ b/js/app/Run.js @@ -20,11 +20,17 @@ * */ -app.run(function ($document, $rootScope, $transitions) { +app.run(function ($document, $rootScope, $transitions, BoardService) { 'use strict'; $document.click(function (event) { $rootScope.$broadcast('documentClicked', event); }); + $transitions.onEnter({from: 'list'}, function($state, $transition$) { + BoardService.unsetCurrrent(); + }); + $transitions.onEnter({to: 'list'}, function($state, $transition$) { + BoardService.unsetCurrrent(); + }); $transitions.onEnter({to: 'board.card'}, function ($state, $transition$) { $rootScope.sidebar.show = true; }); @@ -40,9 +46,6 @@ app.run(function ($document, $rootScope, $transitions) { $transitions.onExit({from: 'board.detail'}, function ($state) { $rootScope.sidebar.show = false; }); - $transitions.onEnter({to: 'board.archive'}, function ($state) { - //BoardController.loadArchived(); - }); $('link[rel="shortcut icon"]').attr( 'href', diff --git a/js/controller/BoardController.js b/js/controller/BoardController.js index 87895e805..46260f2eb 100644 --- a/js/controller/BoardController.js +++ b/js/controller/BoardController.js @@ -80,7 +80,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St } }; $scope.checkCanEdit = function () { - return !$scope.archived; + return !BoardService.getCurrent().archived; }; // filter cards here, as ng-sortable will not work nicely with html-inline filters @@ -114,7 +114,6 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St // Handle initial Loading BoardService.fetchOne($scope.id).then(function (data) { - BoardService.getPermissions(); $scope.statusservice.releaseWaiting(); }, function (error) { $scope.statusservice.setError('Error occured', error); diff --git a/js/controller/CardController.js b/js/controller/CardController.js index 9aa2e9812..424659160 100644 --- a/js/controller/CardController.js +++ b/js/controller/CardController.js @@ -48,9 +48,10 @@ app.controller('CardController', function ($scope, $rootScope, $routeParams, $lo } }; $scope.cardEditDescriptionShow = function($event) { + if(BoardService.isArchived() || CardService.getCurrent().archived) { + return false; + } var node = $event.target.nodeName; - console.log($event); - console.log(BoardService); if($scope.card.archived || !$scope.boardservice.canEdit()) { console.log(node); } else { diff --git a/js/controller/ListController.js b/js/controller/ListController.js index 7389aab35..c4b76388e 100644 --- a/js/controller/ListController.js +++ b/js/controller/ListController.js @@ -1,4 +1,3 @@ - /* * @copyright Copyright (c) 2016 Julius Härtl * @@ -21,11 +20,15 @@ * */ -app.controller('ListController', function ($scope, $location, $filter, BoardService, $element, $timeout) { +/* global app angular */ + +app.controller('ListController', function ($scope, $location, $filter, BoardService, $element, $timeout, $stateParams, $state) { $scope.boards = []; $scope.newBoard = {}; $scope.status = { - deleteUndo: [] + deleteUndo: [], + filter: $stateParams.filter ? $stateParams.filter : '', + sidebar: false }; $scope.colors = ['0082c9', '00c9c6','00c906', 'c92b00', 'F1DB50', '7C31CC', '3A3B3D', 'CACBCD']; $scope.boardservice = BoardService; @@ -42,13 +45,42 @@ app.controller('ListController', function ($scope, $location, $filter, BoardServ $scope.filterData = function () { angular.copy($scope.boardservice.getData(), $scope.boardservice.sorted); - $scope.boardservice.sorted = $filter('orderBy')($scope.boardservice.sorted, 'title'); + angular.copy($scope.boardservice.sorted, $scope.boardservice.sidebar); + $scope.boardservice.sidebar = $filter('orderBy')($scope.boardservice.sidebar, 'title'); + $scope.boardservice.sidebar = $filter('cardFilter')($scope.boardservice.sidebar, {archived: false}); + + if ($scope.status.filter === 'archived') { + var filter = {}; + filter[$scope.status.filter] = true; + $scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, filter); + } else if ($scope.status.filter === 'shared') { + $scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, {archived: false}); + $scope.boardservice.sorted = $filter('boardFilterAcl')($scope.boardservice.sorted); + } else { + $scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, {archived: false}); + } + $scope.boardservice.sorted = $filter('orderBy')($scope.boardservice.sorted, ['deletedAt', 'title']); }; + $scope.$watchCollection(function(){ + return $state.params; + }, function(){ + $scope.status.filter = $state.params.filter; + $scope.filterData(); + }); + + $scope.selectColor = function(color) { $scope.newBoard.color = color; }; + $scope.gotoBoard = function(board) { + if(board.deletedAt > 0) { + return false; + } + return $state.go('board', {boardId: board.id}); + }; + $scope.boardCreate = function() { if(!$scope.newBoard.title || !$scope.newBoard.color) { $scope.status.addBoard=false; @@ -72,26 +104,30 @@ app.controller('ListController', function ($scope, $location, $filter, BoardServ board.status.edit = false; }; + $scope.boardArchive = function (board) { + board.archived = true; + BoardService.update(board).then(function(data) { + $scope.filterData(); + }); + }; + + $scope.boardUnarchive = function (board) { + board.archived = false; + BoardService.update(board).then(function(data) { + $scope.filterData(); + }); + }; + $scope.boardDelete = function(board) { - var boardId = board.id; - $scope.status.deleteUndo[boardId] = 10; - $scope.boardDeleteCountdown = function () { - console.log($scope.status); - if($scope.status.deleteUndo[boardId] > 0) { - $scope.status.deleteUndo[boardId]--; - $timeout($scope.boardDeleteCountdown, 1000); - } - if($scope.status.deleteUndo[boardId] === 0) { - BoardService.delete(board.id).then(function (data) { - $scope.filterData(); - }); - } - }; - $timeout($scope.boardDeleteCountdown, 1000); + BoardService.delete(board.id).then(function (data) { + $scope.filterData(); + }); }; $scope.boardDeleteUndo = function (board) { - delete $scope.status.deleteUndo[board.id]; + BoardService.deleteUndo(board.id).then(function (data) { + $scope.filterData(); + }); }; }); diff --git a/js/filters/boardFilterAcl.js b/js/filters/boardFilterAcl.js new file mode 100644 index 000000000..ef023823f --- /dev/null +++ b/js/filters/boardFilterAcl.js @@ -0,0 +1,33 @@ +/* + * @copyright Copyright (c) 2017 Julius Härtl + * + * @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 . + * + */ + +app.filter('boardFilterAcl', function() { + return function(boards) { + var _result = []; + angular.forEach(boards, function(board){ + if(board.acl !== null && Object.keys(board.acl).length > 0) { + _result.push(board); + } + }); + return _result; + }; +}); \ No newline at end of file diff --git a/js/filters/cardFilter.js b/js/filters/cardFilter.js index c0f1657fb..0f50b7eed 100644 --- a/js/filters/cardFilter.js +++ b/js/filters/cardFilter.js @@ -24,15 +24,16 @@ app.filter('cardFilter', function() { return function(cards, rules) { - var _result = {}; + var _result = []; angular.forEach(cards, function(card){ var _card = card; - angular.some(rules, function(rule, condition) { - if(_card[rule]===condition) { + var keys = Object.keys(rules); + keys.some(function(key, condition) { + if(_card[key]===rules[key]) { _result.push(_card); } }); }); - return result; + return _result; }; }); \ No newline at end of file diff --git a/js/filters/textColorFilter.js b/js/filters/textColorFilter.js index 75b76827c..caaf53ae2 100644 --- a/js/filters/textColorFilter.js +++ b/js/filters/textColorFilter.js @@ -24,7 +24,7 @@ app.filter('textColorFilter', function() { return function (hex) { // RGB2HLS by Garry Tan // http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c - var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex); + var result = /^#?([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex); var color = result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), diff --git a/js/service/ApiService.js b/js/service/ApiService.js index e061efa8c..cbc394d6a 100644 --- a/js/service/ApiService.js +++ b/js/service/ApiService.js @@ -145,7 +145,13 @@ app.factory('ApiService', function($http, $q){ return this.data[this.id]; }; - ApiService.prototype.getData = function() { + ApiService.prototype.unsetCurrrent = function () { + this.id = null; + }; + + + + ApiService.prototype.getData = function() { return $.map(this.data, function(value, index) { return [value]; }); diff --git a/js/service/BoardService.js b/js/service/BoardService.js index b359b1730..d41c024c0 100644 --- a/js/service/BoardService.js +++ b/js/service/BoardService.js @@ -26,6 +26,33 @@ app.factory('BoardService', function(ApiService, $http, $q){ }; BoardService.prototype = angular.copy(ApiService.prototype); + BoardService.prototype.delete = function (id) { + var deferred = $q.defer(); + var self = this; + + $http.delete(this.baseUrl + '/' + id).then(function (response) { + self.data[id].deletedAt = response.data.deletedAt; + deferred.resolve(response.data); + }, function (error) { + deferred.reject('Deleting ' + self.endpoint + ' failed'); + }); + return deferred.promise; + }; + + BoardService.prototype.deleteUndo = function (id) { + var deferred = $q.defer(); + var self = this; + var _id = id; + $http.post(this.baseUrl + '/' + id + '/deleteUndo').then(function (response) { + self.data[_id].deletedAt = 0; + console.log(self.data[_id]); + deferred.resolve(response.data); + }, function (error) { + deferred.reject('Deleting ' + self.endpoint + ' failed'); + }); + return deferred.promise; + }; + BoardService.prototype.searchUsers = function (search) { var deferred = $q.defer(); var self = this; @@ -151,44 +178,43 @@ app.factory('BoardService', function(ApiService, $http, $q){ return deferred.promise; }; - BoardService.prototype.getPermissions = function() { - var board = this.getCurrent(); - var deferred = $q.defer(); - $http.get(this.baseUrl + '/' + board.id + '/permissions').then(function (response) { - board.permissions = response.data; - deferred.resolve(response.data); - }, function (error) { - deferred.reject('Error fetching board permissions ' + board); - }); - }; - BoardService.prototype.canRead = function() { if(!this.getCurrent() || !this.getCurrent().permissions) { return false; } return this.getCurrent().permissions['PERMISSION_READ']; - } + }; BoardService.prototype.canEdit = function() { if(!this.getCurrent() || !this.getCurrent().permissions) { return false; } return this.getCurrent().permissions['PERMISSION_EDIT']; - } + }; - BoardService.prototype.canManage = function() { + BoardService.prototype.canManage = function(board) { + if(board !== null && board !== undefined) { + return board.permissions['PERMISSION_MANAGE']; + } if(!this.getCurrent() || !this.getCurrent().permissions) { return false; } return this.getCurrent().permissions['PERMISSION_MANAGE']; - } + }; BoardService.prototype.canShare = function() { if(!this.getCurrent() || !this.getCurrent().permissions) { return false; } return this.getCurrent().permissions['PERMISSION_SHARE']; - } + }; + + BoardService.prototype.isArchived = function () { + if(!this.getCurrent() || this.getCurrent().archived) { + return true; + } + return false; + }; service = new BoardService($http, 'boards', $q); return service; diff --git a/lib/CardArchivedException.php b/lib/ArchivedItemException.php similarity index 88% rename from lib/CardArchivedException.php rename to lib/ArchivedItemException.php index 4586f7813..352c9452f 100644 --- a/lib/CardArchivedException.php +++ b/lib/ArchivedItemException.php @@ -23,12 +23,12 @@ namespace OCA\Deck; -class CardArchivedException extends \Exception { +class ArchivedItemException extends \Exception { /** * Constructor * @param string $msg the error message */ - public function __construct($msg = "") { + public function __construct($msg = "Operation not allowed. Item is archived.") { parent::__construct($msg); } } \ No newline at end of file diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index 3a8df7c23..f497455d0 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -96,8 +96,8 @@ class BoardController extends Controller { * @param $color * @return \OCP\AppFramework\Db\Entity */ - public function update($id, $title, $color) { - return $this->boardService->update($id, $title, $color); + public function update($id, $title, $color, $archived) { + return $this->boardService->update($id, $title, $color, $archived); } /** @@ -108,6 +108,14 @@ class BoardController extends Controller { public function delete($boardId) { return $this->boardService->delete($boardId); } + /** + * @NoAdminRequired + * @param $boardId + * @return \OCP\AppFramework\Db\Entity + */ + public function deleteUndo($boardId) { + return $this->boardService->deleteUndo($boardId); + } /** * @NoAdminRequired diff --git a/lib/Cron/DeleteCron.php b/lib/Cron/DeleteCron.php new file mode 100644 index 000000000..cdf35cedb --- /dev/null +++ b/lib/Cron/DeleteCron.php @@ -0,0 +1,49 @@ + + * + * @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 . + * + */ + +/** + * Created by PhpStorm. + * User: jus + * Date: 16.05.17 + * Time: 12:34 + */ + +namespace OCA\Deck\Cron; + +use OC\BackgroundJob\Job; +use OCA\Deck\Db\BoardMapper; + +class DeleteCron extends Job { + + public function __construct(BoardMapper $boardMapper) { + $this->boardMapper = $boardMapper; + } + + protected function run($argument) { + $boards = $this->boardMapper->findToDelete(); + foreach ($boards as $board) { + $this->boardMapper->delete($board); + } + } + +} \ No newline at end of file diff --git a/lib/Db/Board.php b/lib/Db/Board.php index 4d3b5e42d..e5531fea8 100644 --- a/lib/Db/Board.php +++ b/lib/Db/Board.php @@ -34,15 +34,19 @@ class Board extends RelationalEntity implements JsonSerializable { protected $archived = false; protected $labels = []; protected $acl = []; + protected $permissions = []; protected $shared; + protected $deletedAt = 0; public function __construct() { $this->addType('id', 'integer'); $this->addType('shared', 'integer'); $this->addType('archived', 'boolean'); + $this->addType('deletedAt', 'integer'); $this->addRelation('labels'); $this->addRelation('acl'); $this->addRelation('shared'); + $this->addRelation('permissions'); $this->addResolvable('owner'); $this->shared = -1; } diff --git a/lib/Db/BoardMapper.php b/lib/Db/BoardMapper.php index e1b2d0598..0c16d9d6c 100644 --- a/lib/Db/BoardMapper.php +++ b/lib/Db/BoardMapper.php @@ -59,7 +59,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { * @return \OCP\AppFramework\Db\Entity if not found */ public function find($id, $withLabels = false, $withAcl = false) { - $sql = 'SELECT id, title, owner, color, archived FROM `*PREFIX*deck_boards` ' . + $sql = 'SELECT id, title, owner, color, archived, deleted_at FROM `*PREFIX*deck_boards` ' . 'WHERE `id` = ?'; $board = $this->findEntity($sql, [$id]); @@ -87,8 +87,8 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { * @return array */ public function findAllByUser($userId, $limit = null, $offset = null) { - $sql = 'SELECT id, title, owner, color, archived, 0 as shared FROM `*PREFIX*deck_boards` WHERE owner = ? UNION ' . - 'SELECT boards.id, title, owner, color, archived, 1 as shared FROM `*PREFIX*deck_boards` as boards ' . + $sql = 'SELECT id, title, owner, color, archived, deleted_at, 0 as shared FROM `*PREFIX*deck_boards` WHERE owner = ? UNION ' . + 'SELECT boards.id, title, owner, color, archived, deleted_at, 1 as shared FROM `*PREFIX*deck_boards` as boards ' . 'JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE acl.participant=? AND acl.type=? AND boards.owner != ?'; $entries = $this->findEntities($sql, [$userId, $userId, Acl::PERMISSION_TYPE_USER, $userId], $limit, $offset); /* @var Board $entry */ @@ -112,7 +112,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { if (count($groups) <= 0) { return []; } - $sql = 'SELECT boards.id, title, owner, color, archived, 2 as shared FROM `*PREFIX*deck_boards` as boards ' . + $sql = 'SELECT boards.id, title, owner, color, archived, deleted_at, 2 as shared FROM `*PREFIX*deck_boards` as boards ' . 'INNER JOIN `*PREFIX*deck_board_acl` as acl ON boards.id=acl.board_id WHERE owner != ? AND type=? AND ('; for ($i = 0; $i < count($groups); $i++) { $sql .= 'acl.participant = ? '; @@ -135,6 +135,15 @@ class BoardMapper extends DeckMapper implements IPermissionMapper { return $this->findEntities($sql, []); } + public function findToDelete() { + // add buffer of 5 min + $timeLimit = time()-(60*5); + $sql = 'SELECT id, title, owner, color, archived, deleted_at FROM `*PREFIX*deck_boards` ' . + 'WHERE `deleted_at` > 0 AND `deleted_at` < ?'; + \OC::$server->getLogger()->error($sql); + return $this->findEntities($sql, [$timeLimit]); + } + public function delete(/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ \OCP\AppFramework\Db\Entity $entity) { // delete acl diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 5a8af4f88..9e3f9948f 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -23,9 +23,12 @@ namespace OCA\Deck\Service; +use OCA\Deck\ArchivedItemException; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\IPermissionMapper; use OCA\Deck\Db\Label; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\IL10N; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; @@ -61,6 +64,13 @@ class BoardService { $this->boardMapper->mapAcl($acl); } } + $permissions = $this->permissionService->matchPermissions($item); + $item->setPermissions([ + 'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ], + 'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT], + 'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE], + 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] + ]); $result[$item->getId()] = $item; } } @@ -77,9 +87,50 @@ class BoardService { $this->boardMapper->mapAcl($acl); } } + $permissions = $this->permissionService->matchPermissions($board); + $board->setPermissions([ + 'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ], + 'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT], + 'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE], + 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] + ]); return $board; } + public function isArchived($mapper, $id) { + try { + if ($mapper instanceof IPermissionMapper) { + $boardId = $mapper->findBoardId($id); + } else { + $boardId = $id; + } + if ($boardId === null) { + return false; + } + } catch (DoesNotExistException $exception) { + return false; + } + $board = $this->find($boardId); + return $board->getArchived(); + } + + public function isDeleted($mapper, $id) { + try { + if ($mapper instanceof IPermissionMapper) { + $boardId = $mapper->findBoardId($id); + } else { + $boardId = $id; + } + if ($boardId === null) { + return false; + } + } catch (DoesNotExistException $exception) { + return false; + } + $board = $this->find($boardId); + return $board->getDeletedAt() > 0; + } + public function create($title, $userId, $color) { @@ -106,20 +157,44 @@ class BoardService { } $new_board->setLabels($labels); $this->boardMapper->mapOwner($new_board); + $permissions = $this->permissionService->matchPermissions($new_board); + $new_board->setPermissions([ + 'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ], + 'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT], + 'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE], + 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] + ]); return $new_board; } public function delete($id) { $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); - return $this->boardMapper->delete($this->find($id)); + $board = $this->find($id); + $board->setDeletedAt(time()); + $this->boardMapper->update($board); + return $board; } - public function update($id, $title, $color) { + public function deleteUndo($id) { + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + $board = $this->find($id); + $board->setDeletedAt(0); + $this->boardMapper->update($board); + } + + public function deleteForce($id) { + $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); + $board = $this->find($id); + return $this->boardMapper->delete($board); + } + + public function update($id, $title, $color, $archived) { $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE); $board = $this->find($id); $board->setTitle($title); $board->setColor($color); + $board->setArchived($archived); $this->boardMapper->mapOwner($board); return $this->boardMapper->update($board); } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 7fd7a6b36..d841d02bc 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -26,18 +26,22 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Acl; -use OCA\Deck\CardArchivedException; use OCA\Deck\Db\StackMapper; +use OCA\Deck\StatusException; class CardService { private $cardMapper; + private $stackMapper; + private $permissionService; + private $boardService; - public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService) { + public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService) { $this->cardMapper = $cardMapper; $this->stackMapper = $stackMapper; $this->permissionService = $permissionService; + $this->boardService = $boardService; } public function find($cardId) { @@ -51,6 +55,9 @@ class CardService { */ public function create($title, $stackId, $type, $order, $owner) { $this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->stackMapper, $stackId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = new Card(); $card->setTitle($title); $card->setStackId($stackId); @@ -63,14 +70,20 @@ class CardService { public function delete($id) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } return $this->cardMapper->delete($this->cardMapper->find($id)); } public function update($id, $title, $stackId, $type, $order, $description, $owner) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($id); if ($card->getArchived()) { - throw new CardArchivedException(); + throw new StatusException('Operation not allowed. This card is archived.'); } $card->setTitle($title); $card->setStackId($stackId); @@ -83,9 +96,12 @@ class CardService { public function rename($id, $title) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($id); if ($card->getArchived()) { - throw new CardArchivedException(); + throw new StatusException('Operation not allowed. This card is archived.'); } $card->setTitle($title); return $this->cardMapper->update($card); @@ -93,12 +109,15 @@ class CardService { public function reorder($id, $stackId, $order) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $cards = $this->cardMapper->findAll($stackId); $result = []; $i = 0; foreach ($cards as $card) { if ($card->getArchived()) { - throw new CardArchivedException(); + throw new StatusException('Operation not allowed. This card is archived.'); } if ($card->id === $id) { $card->setOrder($order); @@ -121,6 +140,9 @@ class CardService { public function archive($id) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($id); $card->setArchived(true); return $this->cardMapper->update($card); @@ -128,6 +150,9 @@ class CardService { public function unarchive($id) { $this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($id); $card->setArchived(false); return $this->cardMapper->update($card); @@ -135,18 +160,24 @@ class CardService { public function assignLabel($cardId, $labelId) { $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $cardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($cardId); if ($card->getArchived()) { - throw new CardArchivedException(); + throw new StatusException('Operation not allowed. This card is archived.'); } $this->cardMapper->assignLabel($cardId, $labelId); } public function removeLabel($cardId, $labelId) { $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + if($this->boardService->isArchived($this->cardMapper, $cardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $card = $this->cardMapper->find($cardId); if ($card->getArchived()) { - throw new CardArchivedException(); + throw new StatusException('Operation not allowed. This card is archived.'); } $this->cardMapper->removeLabel($cardId, $labelId); } diff --git a/lib/Service/LabelService.php b/lib/Service/LabelService.php index e1538adff..5562a60ca 100644 --- a/lib/Service/LabelService.php +++ b/lib/Service/LabelService.php @@ -26,15 +26,22 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Label; use OCA\Deck\Db\Acl; use OCA\Deck\Db\LabelMapper; +use OCA\Deck\StatusException; class LabelService { + /** @var LabelMapper */ private $labelMapper; + /** @var PermissionService */ + private $permissionService; + /** @var BoardService */ + private $boardService; - public function __construct(LabelMapper $labelMapper, PermissionService $permissionService) { + public function __construct(LabelMapper $labelMapper, PermissionService $permissionService, BoardService $boardService) { $this->labelMapper = $labelMapper; $this->permissionService = $permissionService; + $this->boardService = $boardService; } public function find($labelId) { @@ -44,6 +51,9 @@ class LabelService { public function create($title, $color, $boardId) { $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE); + if($this->boardService->isArchived(null, $boardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $label = new Label(); $label->setTitle($title); $label->setColor($color); @@ -53,11 +63,17 @@ class LabelService { public function delete($id) { $this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE); + if($this->boardService->isArchived($this->labelMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } return $this->labelMapper->delete($this->find($id)); } public function update($id, $title, $color) { $this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE); + if($this->boardService->isArchived($this->labelMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $label = $this->find($id); $label->setTitle($title); $label->setColor($color); diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index bd3985a51..c877d7134 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -25,6 +25,7 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\IPermissionMapper; use OCA\Deck\NoPermissionException; @@ -50,7 +51,7 @@ class PermissionService { } /** - * Get current user permissions for a board + * Get current user permissions for a board by id * * @param $boardId * @return bool|array @@ -66,6 +67,24 @@ class PermissionService { ]; } + /** + * Get current user permissions for a board + * + * @param Board $board + * @return array|bool + * @internal param $boardId + */ + public function matchPermissions(Board $board) { + $owner = $this->userIsBoardOwner($board->getId()); + $acls = $board->getAcl(); + return [ + Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ), + Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT), + Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE), + Acl::PERMISSION_SHARE => $owner || $this->userCan($acls, Acl::PERMISSION_SHARE), + ]; + } + /** * check permissions for replacing dark magic middleware * diff --git a/lib/Service/StackService.php b/lib/Service/StackService.php index 7c94ee3ce..18cb95cbb 100644 --- a/lib/Service/StackService.php +++ b/lib/Service/StackService.php @@ -31,6 +31,7 @@ use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use OCA\Deck\StatusException; class StackService { @@ -39,12 +40,14 @@ class StackService { private $cardMapper; private $labelMapper; private $permissionService; + private $boardService; - public function __construct(StackMapper $stackMapper, CardMapper $cardMapper, LabelMapper $labelMapper, PermissionService $permissionService) { + public function __construct(StackMapper $stackMapper, CardMapper $cardMapper, LabelMapper $labelMapper, PermissionService $permissionService, BoardService $boardService) { $this->stackMapper = $stackMapper; $this->cardMapper = $cardMapper; $this->labelMapper = $labelMapper; $this->permissionService = $permissionService; + $this->boardService = $boardService; } public function findAll($boardId) { @@ -81,6 +84,9 @@ class StackService { public function create($title, $boardId, $order) { $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE); + if($this->boardService->isArchived(null, $boardId)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $stack = new Stack(); $stack->setTitle($title); $stack->setBoardId($boardId); @@ -96,6 +102,9 @@ class StackService { public function update($id, $title, $boardId, $order) { $this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE); + if($this->boardService->isArchived($this->stackMapper, $id)) { + throw new StatusException('Operation not allowed. This board is archived.'); + } $stack = $this->stackMapper->find($id); $stack->setTitle($title); $stack->setBoardId($boardId); diff --git a/templates/main.php b/templates/main.php index a22fe8132..ed014caba 100644 --- a/templates/main.php +++ b/templates/main.php @@ -45,7 +45,7 @@ if(!\OC::$server->getConfig()->getSystemValue('debug', false)) { 'app' => ['App', 'Config', 'Run'], 'controller' => ['AppController', 'BoardController', 'CardController', 'ListController'], 'directive' => ['appnavigationentryutils', 'appPopoverMenuUtils', 'autofocusoninsert', 'avatar', 'elastic', 'search'], - 'filters' => ['cardFilter', 'cardSearchFilter', 'iconWhiteFilter', 'lightenColorFilter', 'orderObjectBy', 'relativeDateFilter', 'textColorFilter'], + 'filters' => ['boardFilterAcl', 'cardFilter', 'cardSearchFilter', 'iconWhiteFilter', 'lightenColorFilter', 'orderObjectBy', 'relativeDateFilter', 'textColorFilter'], 'service' => ['ApiService', 'BoardService', 'CardService', 'LabelService', 'StackService', 'StatusService'], ]; foreach($js as $folder=>$files) { @@ -58,7 +58,7 @@ if(!\OC::$server->getConfig()->getSystemValue('debug', false)) {
-
+
inc('part.navigation')); ?> inc('part.settings')); */ ?>
diff --git a/templates/part.board.mainView.php b/templates/part.board.mainView.php index 92875a26b..ede5fedb8 100644 --- a/templates/part.board.mainView.php +++ b/templates/part.board.mainView.php @@ -64,46 +64,40 @@
- -
- -
+
-
+
- -
+
diff --git a/templates/part.board.sidebarView.php b/templates/part.board.sidebarView.php index cb54dbfab..dbcb81767 100644 --- a/templates/part.board.sidebarView.php +++ b/templates/part.board.sidebarView.php @@ -9,6 +9,7 @@

{{ boardservice.getCurrent().title }}

+{{board=boardservice.getCurrent();""}}
  • t('Sharing')); ?>
  • @@ -90,7 +91,7 @@
    -

    +

    diff --git a/templates/part.boardlist.php b/templates/part.boardlist.php index 1d6940f97..2f3df25df 100644 --- a/templates/part.boardlist.php +++ b/templates/part.boardlist.php @@ -5,31 +5,66 @@
+ - - + - + - - - + + + + + + + + +
t('Title')); ?> t('Members')); ?>
+
{{ b.title }}{{ b.title }}
+
+ +
+ +
+
t('Create new board')); ?> -
+
+ -
+ ng-class="{'selected': (c == newBoard.color), 'dark': (newBoard.color | textColorFilter) === '#ffffff' }"> - +
diff --git a/templates/part.card.php b/templates/part.card.php index 9257f8efe..c423a6859 100644 --- a/templates/part.card.php +++ b/templates/part.card.php @@ -31,6 +31,7 @@
-
  • t('All Boards')); ?>
  • +
  • t('All Boards')); ?>
  • +
  • t('Archived boards')); ?>
  • +
  • t('Shared boards')); ?>
  • -
  • +
  • {{ b.title }}
      -
    • Deleting in {{ status.deleteUndo[b.id] }}s   X
      -
    • -
    • -
    • +
    • +
    -
    -
    Deleted X
    - -
    @@ -43,7 +39,7 @@
    -

    +

    diff --git a/tests/integration/database/BoardDatabaseTest.php b/tests/integration/database/BoardDatabaseTest.php index f0df9e7bd..f534055bb 100644 --- a/tests/integration/database/BoardDatabaseTest.php +++ b/tests/integration/database/BoardDatabaseTest.php @@ -24,7 +24,7 @@ /** * @group DB */ -class BoardDatabaseTest extends \PHPUnit_Framework_TestCase +class BoardDatabaseTest extends \Test\TestCase { const TEST_USER1 = "test-share-user1"; const TEST_USER2 = "test-share-user2"; @@ -61,6 +61,7 @@ class BoardDatabaseTest extends \PHPUnit_Framework_TestCase \OC::$server->getGroupManager()->addBackend($groupBackend); } public function setUp() { + parent::setUp(); \OC::$server->getUserSession()->login(self::TEST_USER1, self::TEST_USER1); $this->boardService = \OC::$server->query("\OCA\Deck\Service\BoardService"); } @@ -76,9 +77,10 @@ class BoardDatabaseTest extends \PHPUnit_Framework_TestCase $this->assertEquals($actual->getTitle(), $board->getTitle()); $this->assertEquals($actual->getColor(), $board->getColor()); $this->assertEquals($actual->getOwner(), $board->getOwner()); - $this->boardService->delete($id); + $this->boardService->deleteForce($id); } public function tearDown() { + parent::tearDown(); } } \ No newline at end of file diff --git a/tests/unit/Cron/DeleteCronTest.php b/tests/unit/Cron/DeleteCronTest.php new file mode 100644 index 000000000..ff3aba488 --- /dev/null +++ b/tests/unit/Cron/DeleteCronTest.php @@ -0,0 +1,72 @@ + + * + * @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\Cron; + +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; + +class DeleteCronTest extends \Test\TestCase { + + /** @var BoardMapper|\PHPUnit_Framework_MockObject_MockObject */ + protected $boardMapper; + /** @var DeleteCron */ + protected $deleteCron; + + public function setUp() { + parent::setUp(); + $this->boardMapper = $this->createMock(BoardMapper::class); + $this->deleteCron = new DeleteCron($this->boardMapper); + } + + protected function getBoard($id) { + $board = new Board(); + $board->setId($id); + return $board; + } + + public function testDeleteCron() { + $boards = [ + $this->getBoard(1), + $this->getBoard(2), + $this->getBoard(3), + $this->getBoard(4), + ]; + $this->boardMapper->expects($this->once()) + ->method('findToDelete') + ->willReturn($boards); + $this->boardMapper->expects($this->at(1)) + ->method('delete') + ->with($boards[0]); + $this->boardMapper->expects($this->at(2)) + ->method('delete') + ->with($boards[1]); + $this->boardMapper->expects($this->at(3)) + ->method('delete') + ->with($boards[2]); + $this->boardMapper->expects($this->at(4)) + ->method('delete') + ->with($boards[3]); + $this->invokePrivate($this->deleteCron, 'run', [null]); + } +} \ No newline at end of file diff --git a/tests/unit/Db/BoardMapperTest.php b/tests/unit/Db/BoardMapperTest.php index 9b3e87d33..ab865b1fb 100644 --- a/tests/unit/Db/BoardMapperTest.php +++ b/tests/unit/Db/BoardMapperTest.php @@ -23,6 +23,7 @@ namespace OCA\Deck\Db; +use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; use Test\AppFramework\Db\MapperTestUtility; @@ -32,10 +33,15 @@ use Test\AppFramework\Db\MapperTestUtility; */ class BoardMapperTest extends MapperTestUtility { + /** @var IDBConnection */ private $dbConnection; + /** @var AclMapper|\PHPUnit_Framework_MockObject_MockObject */ private $aclMapper; + /** @var BoardMapper */ private $boardMapper; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ private $userManager; + /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */ private $groupManager; // Data @@ -72,7 +78,7 @@ class BoardMapperTest extends MapperTestUtility { $this->aclMapper->insert($this->getAcl('user','user1', false, false, false, $this->boards[2]->getId())) ]; - foreach ($this->acls as $acl) { + foreach ($this->acls as $acl) { $acl->resetUpdatedFields(); } foreach ($this->boards as $board) { @@ -101,14 +107,55 @@ class BoardMapperTest extends MapperTestUtility { public function testFind() { $actual = $this->boardMapper->find($this->boards[0]->getId()); - $expected = $this->boards[0]; + /** @var Board $expected */ + $expected = clone $this->boards[0]; + $expected->setShared(-1); + $expected->resetUpdatedFields(); $this->assertEquals($expected, $actual); } + + public function testFindAllByUser() { + $actual = $this->boardMapper->findAllByUser('user1'); + $expected = [ + $this->boards[0], + $this->boards[1], + $this->boards[2] + ]; + foreach ($expected as $e) { + foreach ($actual as $a) { + if($e->getId() === $a->getId()) { + $this->assertEquals($e->getTitle(), $a->getTitle()); + } + } + } + } + + public function testFindAll() { + $actual = $this->boardMapper->findAll(); + $this->assertEquals($this->boards[0]->getId(), $actual[0]->getId()); + $this->assertEquals($this->boards[1]->getId(), $actual[1]->getId()); + $this->assertEquals($this->boards[2]->getId(), $actual[2]->getId()); + } + + public function testFindAllToDelete() { + $this->boards[0]->setDeletedAt(1); + $this->boards[0] = $this->boardMapper->update($this->boards[0]); + + $actual = $this->boardMapper->findToDelete(); + $this->boards[0]->resetUpdatedFields(); + $this->assertEquals([$this->boards[0]], $actual); + + $this->boards[0]->setDeletedAt(0); + $this->boardMapper->update($this->boards[0]); + } + public function testFindWithLabels() { $actual = $this->boardMapper->find($this->boards[0]->getId(), true, false); - $expected = $this->boards[0]; - $this->assertEquals($expected, $actual); + /** @var Board $expected */ + $expected = $this->boards[0]; + $this->assertEquals($expected->getLabels(), $actual->getLabels()); } + public function testFindWithAcl() { $actual = $this->boardMapper->find($this->boards[0]->getId(), false, true); $expected = [ diff --git a/tests/unit/Db/BoardTest.php b/tests/unit/Db/BoardTest.php index 7cbb11d50..99b703976 100644 --- a/tests/unit/Db/BoardTest.php +++ b/tests/unit/Db/BoardTest.php @@ -2,7 +2,9 @@ namespace OCA\Deck\Db; -class BoardTest extends \PHPUnit_Framework_TestCase { +use PHPUnit\Framework\TestCase; + +class BoardTest extends TestCase { private function createBoard() { $board = new Board(); $board->setId(1); @@ -21,6 +23,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase { 'owner' => "admin", 'color' => "000000", 'labels' => array(), + 'permissions' => [], + 'deletedAt' => 0, 'acl' => array(), 'archived' => false ], $board->jsonSerialize()); @@ -35,6 +39,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase { 'owner' => "admin", 'color' => "000000", 'labels' => array("foo", "bar"), + 'permissions' => [], + 'deletedAt' => 0, 'acl' => array(), 'archived' => false ], $board->jsonSerialize()); @@ -56,6 +62,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase { 'owner' => "admin", 'color' => "000000", 'labels' => array(), + 'permissions' => [], + 'deletedAt' => 0, 'acl' => array(), 'archived' => false, 'shared' => 1, diff --git a/tests/unit/Db/GroupTest.php b/tests/unit/Db/GroupTest.php new file mode 100644 index 000000000..cb98d6f0f --- /dev/null +++ b/tests/unit/Db/GroupTest.php @@ -0,0 +1,58 @@ + + * + * @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\Db; + +use OCP\IGroup; + +class GroupTest extends \Test\TestCase { + + public function testGroupObjectSerialize() { + /** @var IGroup $group */ + $group = $this->createMock(IGroup::class); + $group->expects($this->any()) + ->method('getGID') + ->willReturn('mygroup'); + $groupRelationalObject = new Group($group); + $expected = [ + 'uid' => 'mygroup', + 'displayname' => 'mygroup' + ]; + $this->assertEquals($expected, $groupRelationalObject->getObjectSerialization()); + } + + public function testGroupJSONSerialize() { + /** @var IGroup $group */ + $group = $this->createMock(IGroup::class); + $group->expects($this->any()) + ->method('getGID') + ->willReturn('mygroup'); + $groupRelationalObject = new Group($group); + $expected = [ + 'uid' => 'mygroup', + 'displayname' => 'mygroup', + 'primaryKey' => 'mygroup' + ]; + $this->assertEquals($expected, $groupRelationalObject->jsonSerialize()); + } +} \ No newline at end of file diff --git a/tests/unit/Db/UserTest.php b/tests/unit/Db/UserTest.php new file mode 100644 index 000000000..43b15dda1 --- /dev/null +++ b/tests/unit/Db/UserTest.php @@ -0,0 +1,64 @@ + + * + * @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\Db; + +use OCP\IUser; + +class UserTest extends \Test\TestCase { + + public function testGroupObjectSerialize() { + /** @var IUser $user */ + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('myuser'); + $user->expects($this->any()) + ->method('getDisplayName') + ->willReturn('myuser displayname'); + $userRelationalObject = new User($user); + $expected = [ + 'uid' => 'myuser', + 'displayname' => 'myuser displayname' + ]; + $this->assertEquals($expected, $userRelationalObject->getObjectSerialization()); + } + + public function testGroupJSONSerialize() { + /** @var IUser $user */ + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('myuser'); + $user->expects($this->any()) + ->method('getDisplayName') + ->willReturn('myuser displayname'); + $userRelationalObject = new User($user); + $expected = [ + 'uid' => 'myuser', + 'displayname' => 'myuser displayname', + 'primaryKey' => 'myuser' + ]; + $this->assertEquals($expected, $userRelationalObject->jsonSerialize()); + } +} \ No newline at end of file diff --git a/tests/unit/ExceptionsTest.php b/tests/unit/ExceptionsTest.php index 75d2dc1fa..732e31ca6 100644 --- a/tests/unit/ExceptionsTest.php +++ b/tests/unit/ExceptionsTest.php @@ -23,7 +23,7 @@ namespace OCA\Deck\Db; -use OCA\Deck\CardArchivedException; +use OCA\Deck\ArchivedItemException; use OCA\Deck\Controller\PageController; use OCA\Deck\NoPermissionException; use OCA\Deck\NotFoundException; @@ -45,7 +45,7 @@ class ExceptionsTest extends \PHPUnit_Framework_TestCase { } public function testCardArchivedException() { - $e = new CardArchivedException('foo'); + $e = new ArchivedItemException('foo'); $this->assertEquals('foo', $e->getMessage()); } diff --git a/tests/unit/Migration/UnknownUserTest.php b/tests/unit/Migration/UnknownUserTest.php new file mode 100644 index 000000000..5c61e6963 --- /dev/null +++ b/tests/unit/Migration/UnknownUserTest.php @@ -0,0 +1,128 @@ + + * + * @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\Migration; + +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCP\IGroupManager; +use OCP\IUserManager; +use OCP\Migration\IOutput; + +class UnknownUserTest extends \Test\TestCase { + + /** @var IUserManager */ + private $userManager; + /** @var IGroupManager */ + private $groupManager; + /** @var AclMapper */ + private $aclMapper; + /** @var BoardMapper */ + private $boardMapper; + /** @var UnknownUsers */ + private $unknownUsers; + + public function setUp() { + parent::setUp(); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->aclMapper = $this->createMock(AclMapper::class); + $this->boardMapper = $this->createMock(BoardMapper::class); + $this->unknownUsers = new UnknownUsers($this->userManager, $this->groupManager, $this->aclMapper, $this->boardMapper); + } + + + + public function testGetName() { + $this->assertEquals('Delete orphaned ACL rules', $this->unknownUsers->getName()); + } + + public function testRun() { + /** @var IOutput $output */ + $output = $this->createMock(IOutput::class); + $boards = [ + $this->getBoard(1,'Test', 'admin'), + ]; + $acl = [ + $this->getAcl('user', 'existing', 1), + $this->getAcl('user', 'not existing', 1), + $this->getAcl('group', 'existing', 1), + $this->getAcl('group', 'not existing', 1), + ]; + $this->aclMapper->expects($this->at(0)) + ->method('findAll') + ->with(1) + ->willReturn($acl); + + $this->userManager->expects($this->at(0)) + ->method('get') + ->with('existing') + ->willReturn(true); + $this->userManager->expects($this->at(1)) + ->method('get') + ->with('not existing') + ->willReturn(null); + $this->groupManager->expects($this->at(0)) + ->method('get') + ->with('existing') + ->willReturn(true); + $this->groupManager->expects($this->at(1)) + ->method('get') + ->with('not existing') + ->willReturn(null); + + $this->boardMapper->expects($this->once()) + ->method('findAll') + ->willReturn($boards); + + $this->aclMapper->expects($this->at(1)) + ->method('delete') + ->with($acl[1]); + $this->aclMapper->expects($this->at(2)) + ->method('delete') + ->with($acl[3]); + + $this->unknownUsers->run($output); + } + + + /** @return Acl */ + public function getAcl($type='user', $participant='admin', $boardId=123) { + $acl = new Acl(); + $acl->setParticipant($participant); + $acl->setType($type); + $acl->setBoardId($boardId); + return $acl; + } + + /** @return Board */ + public function getBoard($id, $title, $owner) { + $board = new Board(); + $board->setId($id); + $board->setTitle($title); + $board->setOwner($owner); + return $board; + } +} \ No newline at end of file diff --git a/tests/unit/Service/BoardServiceTest.php b/tests/unit/Service/BoardServiceTest.php index a60955123..d8fb6080e 100644 --- a/tests/unit/Service/BoardServiceTest.php +++ b/tests/unit/Service/BoardServiceTest.php @@ -53,6 +53,7 @@ class BoardServiceTest extends \Test\TestCase { private $userId = 'admin'; public function setUp() { + parent::setUp(); $this->l10n = $this->createMock(L10N::class); $this->aclMapper = $this->createMock(AclMapper::class); $this->boardMapper = $this->createMock(BoardMapper::class); @@ -134,11 +135,12 @@ class BoardServiceTest extends \Test\TestCase { ->method('update') ->with($board) ->willReturn($board); - $b = $this->service->update(123, 'MyNewNameBoard', 'ffffff'); + $b = $this->service->update(123, 'MyNewNameBoard', 'ffffff', false); $this->assertEquals($b->getTitle(), 'MyNewNameBoard'); $this->assertEquals($b->getOwner(), 'admin'); $this->assertEquals($b->getColor(), 'ffffff'); + $this->assertEquals($b->getArchived(), false); } public function testDelete() { @@ -147,10 +149,7 @@ class BoardServiceTest extends \Test\TestCase { $this->boardMapper->expects($this->once()) ->method('find') ->willReturn($board); - $this->boardMapper->expects($this->once()) - ->method('delete') - ->willReturn(1); - $this->assertEquals(1, $this->service->delete(123)); + $this->assertEquals($board, $this->service->delete(123)); } public function testAddAcl() { diff --git a/tests/unit/Service/CardServiceTest.php b/tests/unit/Service/CardServiceTest.php index f0f49d8c1..c673ccbee 100644 --- a/tests/unit/Service/CardServiceTest.php +++ b/tests/unit/Service/CardServiceTest.php @@ -27,9 +27,10 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\StackMapper; -use OCA\Deck\CardArchivedException; +use OCA\Deck\StatusException; +use Test\TestCase; -class CardServiceTest extends \PHPUnit_Framework_TestCase { +class CardServiceTest extends TestCase { /** @var CardService|\PHPUnit_Framework_MockObject_MockObject */ private $cardService; @@ -39,16 +40,19 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { private $stackMapper; /** @var PermissionService|\PHPUnit_Framework_MockObject_MockObject */ private $permissionService; - + /** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */ + private $boardService; public function setUp() { + parent::setUp(); $this->cardMapper = $this->getMockBuilder(CardMapper::class) ->disableOriginalConstructor()->getMock(); $this->stackMapper = $this->getMockBuilder(StackMapper::class) ->disableOriginalConstructor()->getMock(); $this->permissionService = $this->getMockBuilder(PermissionService::class) ->disableOriginalConstructor()->getMock(); - $this->cardService = new CardService($this->cardMapper, $this->stackMapper, $this->permissionService); + $this->boardService = $this->createMock(BoardService::class); + $this->cardService = new CardService($this->cardMapper, $this->stackMapper, $this->permissionService, $this->boardService); } public function testFind() { @@ -108,7 +112,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { $card->setArchived(true); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->never())->method('update'); - $this->setExpectedException(CardArchivedException::class); + $this->setExpectedException(StatusException::class); $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin'); } @@ -128,7 +132,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { $card->setArchived(true); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->never())->method('update'); - $this->setExpectedException(CardArchivedException::class); + $this->setExpectedException(StatusException::class); $this->cardService->rename(123, 'newtitle'); } @@ -168,7 +172,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { $card->setArchived(true); $this->cardMapper->expects($this->once())->method('findAll')->willReturn([$card]); $this->cardMapper->expects($this->never())->method('update')->willReturnCallback(function($c) { return $c; }); - $this->setExpectedException(CardArchivedException::class); + $this->setExpectedException(StatusException::class); $actual = $this->cardService->reorder(123, 234, 1); } public function testArchive() { @@ -204,7 +208,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { $card->setArchived(true); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->never())->method('assignLabel'); - $this->setExpectedException(CardArchivedException::class); + $this->setExpectedException(StatusException::class); $this->cardService->assignLabel(123, 999); } @@ -221,7 +225,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase { $card->setArchived(true); $this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->never())->method('removeLabel'); - $this->setExpectedException(CardArchivedException::class); + $this->setExpectedException(StatusException::class); $this->cardService->removeLabel(123, 999); } diff --git a/tests/unit/Service/LabelServiceTest.php b/tests/unit/Service/LabelServiceTest.php index 561b07859..8f266218c 100644 --- a/tests/unit/Service/LabelServiceTest.php +++ b/tests/unit/Service/LabelServiceTest.php @@ -26,8 +26,9 @@ namespace OCA\Deck\Service; use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; +use Test\TestCase; -class LabelServiceTest extends \PHPUnit_Framework_TestCase { +class LabelServiceTest extends TestCase { /** @var LabelMapper|\PHPUnit_Framework_MockObject_MockObject */ private $labelMapper; @@ -35,15 +36,20 @@ class LabelServiceTest extends \PHPUnit_Framework_TestCase { private $permissionService; /** @var LabelService */ private $labelService; + /** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */ + private $boardService; public function setUp() { + parent::setUp(); $this->labelMapper = $this->getMockBuilder(LabelMapper::class) ->disableOriginalConstructor()->getMock(); $this->permissionService = $this->getMockBuilder(PermissionService::class) ->disableOriginalConstructor()->getMock(); + $this->boardService = $this->createMock(BoardService::class); $this->labelService = new LabelService( $this->labelMapper, - $this->permissionService + $this->permissionService, + $this->boardService ); } diff --git a/tests/unit/Service/PermissionServiceTest.php b/tests/unit/Service/PermissionServiceTest.php index 15ce8d6cd..43466c9c9 100644 --- a/tests/unit/Service/PermissionServiceTest.php +++ b/tests/unit/Service/PermissionServiceTest.php @@ -46,6 +46,7 @@ class PermissionServiceTest extends \PHPUnit_Framework_TestCase { private $userId = 'admin'; public function setUp() { + parent::setUp(); $this->logger = $this->request = $this->getMockBuilder(ILogger::class) ->disableOriginalConstructor() ->getMock(); diff --git a/tests/unit/Service/StackServiceTest.php b/tests/unit/Service/StackServiceTest.php index ba6c6e5b6..f262deced 100644 --- a/tests/unit/Service/StackServiceTest.php +++ b/tests/unit/Service/StackServiceTest.php @@ -31,8 +31,15 @@ use OCA\Deck\Db\Label; use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\Stack; use OCA\Deck\Db\StackMapper; +use \Test\TestCase; -class StackServiceTest extends \PHPUnit_Framework_TestCase { +/** + * Class StackServiceTest + * + * @package OCA\Deck\Service + * @group DB + */ +class StackServiceTest extends TestCase { /** @var StackService */ private $stackService; @@ -44,8 +51,11 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase { private $labelMapper; /** @var \PHPUnit_Framework_MockObject_MockObject|PermissionService */ private $permissionService; + /** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */ + private $boardService; public function setUp() { + parent::setUp(); $this->stackMapper = $this->getMockBuilder(StackMapper::class) ->disableOriginalConstructor()->getMock(); $this->cardMapper = $this->getMockBuilder(CardMapper::class) @@ -54,12 +64,14 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase { ->disableOriginalConstructor()->getMock(); $this->permissionService = $this->getMockBuilder(PermissionService::class) ->disableOriginalConstructor()->getMock(); + $this->boardService = $this->createMock(BoardService::class); $this->stackService = new StackService( $this->stackMapper, $this->cardMapper, $this->labelMapper, - $this->permissionService + $this->permissionService, + $this->boardService ); } @@ -160,6 +172,9 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($stack, $result); } + /** + * @group DB + */ public function testReorder() { $this->permissionService->expects($this->once())->method('checkPermission'); $a = $this->createStack(1, 0); diff --git a/tests/unit/controller/BoardControllerTest.php b/tests/unit/controller/BoardControllerTest.php index 66b56ca74..d26e9288c 100644 --- a/tests/unit/controller/BoardControllerTest.php +++ b/tests/unit/controller/BoardControllerTest.php @@ -107,9 +107,9 @@ class BoardControllerTest extends \PHPUnit_Framework_TestCase { public function testUpdate() { $this->boardService->expects($this->once()) ->method('update') - ->with(1, 2, 3) + ->with(1, 2, 3, false) ->willReturn(1); - $this->assertEquals(1, $this->controller->update(1, 2, 3)); + $this->assertEquals(1, $this->controller->update(1, 2, 3, false)); } public function testDelete() { @@ -120,6 +120,14 @@ class BoardControllerTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(1, $this->controller->delete(123)); } + public function testDeleteUndo() { + $this->boardService->expects($this->once()) + ->method('deleteUndo') + ->with(123) + ->willReturn(1); + $this->assertEquals(1, $this->controller->deleteUndo(123)); + } + public function testGetUserPermissions() { $acl = [ Acl::PERMISSION_READ => true,