Merge pull request #133 from nextcloud/archive-boards

Archive boards
This commit is contained in:
Julius Härtl
2017-06-08 22:46:04 +02:00
committed by GitHub
46 changed files with 1002 additions and 173 deletions

View File

@@ -67,7 +67,7 @@ pipeline:
matrix: matrix:
TESTS: syntax-php7.0 TESTS: syntax-php7.0
php5.6: php5.6:
image: nextcloudci/php5.6:php5.6-3 image: nextcloudci/php5.6:php5.6-7
environment: environment:
- APP_NAME=deck - APP_NAME=deck
- CORE_BRANCH=master - CORE_BRANCH=master
@@ -93,7 +93,7 @@ pipeline:
matrix: matrix:
TESTS: php5.6 TESTS: php5.6
php7.0: php7.0:
image: nextcloudci/php7.0:php7.0-7 image: nextcloudci/php7.0:php7.0-8
environment: environment:
- APP_NAME=deck - APP_NAME=deck
- CORE_BRANCH=master - CORE_BRANCH=master
@@ -170,4 +170,4 @@ matrix:
- TESTS: jsbuild - TESTS: jsbuild
- TESTS: integration - TESTS: integration
branches: [ master, stable* ]

View File

@@ -38,6 +38,14 @@
<type>boolean</type> <type>boolean</type>
<default>false</default> <default>false</default>
</field> </field>
<field>
<name>deleted_at</name>
<type>integer</type>
<default>0</default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
</declaration> </declaration>
</table> </table>
<table> <table>

View File

@@ -16,7 +16,7 @@
💥 This is still alpha software: it may not be stable enough for production! 💥 This is still alpha software: it may not be stable enough for production!
</description> </description>
<version>0.1.4.1</version> <version>0.1.4.2</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>Julius Härtl</author> <author>Julius Härtl</author>
<namespace>Deck</namespace> <namespace>Deck</namespace>
@@ -30,6 +30,9 @@
<dependencies> <dependencies>
<nextcloud min-version="11" max-version="13" /> <nextcloud min-version="11" max-version="13" />
</dependencies> </dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
</background-jobs>
<repair-steps> <repair-steps>
<post-migration> <post-migration>
<step>OCA\Deck\Migration\UnknownUsers</step> <step>OCA\Deck\Migration\UnknownUsers</step>

View File

@@ -31,6 +31,7 @@ return [
['name' => 'board#read', 'url' => '/boards/{boardId}', 'verb' => 'GET'], ['name' => 'board#read', 'url' => '/boards/{boardId}', 'verb' => 'GET'],
['name' => 'board#update', 'url' => '/boards/{boardId}', 'verb' => 'PUT'], ['name' => 'board#update', 'url' => '/boards/{boardId}', 'verb' => 'PUT'],
['name' => 'board#delete', 'url' => '/boards/{boardId}', 'verb' => 'DELETE'], ['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#getUserPermissions', 'url' => '/boards/{boardId}/permissions', 'verb' => 'GET'],
['name' => 'board#addAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'POST'], ['name' => 'board#addAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'POST'],
['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'PUT'], ['name' => 'board#updateAcl', 'url' => '/boards/{boardId}/acl', 'verb' => 'PUT'],

View File

@@ -65,6 +65,14 @@ button.button-inline:hover {
* Navigation sidebar * 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 { .app-navigation-entry-utils-menu-share {
display: flex !important; display: flex !important;
padding: 14px; padding: 14px;
@@ -318,9 +326,9 @@ button.button-inline:hover {
.popovermenu { .popovermenu {
z-index: 999; z-index: 999;
opacity: 1; opacity: 1;
margin-top: 25px;
margin-right: 3px;
display: block; display: block;
margin-top: 25px;
margin-right: 0px;
} }
.popovermenu.hidden { .popovermenu.hidden {
@@ -614,23 +622,30 @@ button.button-inline:hover {
.colorselect { .colorselect {
overflow: hidden; overflow: hidden;
clear: both; border-radius:3px;
padding-top: 4px; flex-direction: row;
padding-left: 4px; min-width: 240px;
height: 34px;
display: flex;
margin: 3px 3px 3px 0;
} }
.colorselect .color { .colorselect .color {
opacity: 0.7; opacity: 0.7;
width: 27px; height: 100%;
height: 27px; flex-grow: 1;
float: left;
margin-right: 2px;
border: none; border: none;
} }
.colorselect .selected { .colorselect .selected {
background-image: url(../../../core/img/actions/checkmark.svg);
background-position: center center;
background-repeat: no-repeat;
opacity: 1.0; opacity: 1.0;
border: 1px solid #333333; }
.colorselect .dark.selected {
background-image: url(../../../core/img/actions/checkmark-white.svg);
} }
.labels .colorselect { .labels .colorselect {
@@ -662,6 +677,16 @@ button.button-inline:hover {
cursor: pointer; cursor: pointer;
display: block; display: block;
} }
#boardlist .app-popover-menu-utils {
width: 30px;
display: inline;
position: relative;
}
.popovermenu ul {
display: flex !important;
flex-direction: column;
}
#boardlist td { #boardlist td {
padding: 10px; padding: 10px;
@@ -680,15 +705,34 @@ button.button-inline:hover {
.cell-board-title { .cell-board-title {
width: 50%; width: 50%;
} }
#boardlist .colorselect, #boardlist tr.deleted td * {
#boardlist input { opacity: 0.5;
float: left;
} }
#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; margin-top: 5px;
} }
@@ -925,6 +969,10 @@ button.button-inline:hover {
* Custom icons * Custom icons
*/ */
.icon-deck {
background-image: url(../img/deck.svg);
}
.icon-group { .icon-group {
background-image: url('../../../settings/img/users.svg'); background-image: url('../../../settings/img/users.svg');
} }

1
img/deck.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M432.3 448.1h-348c-13.2 0-24-10.8-24-24V264.6c0-13.2 10.8-24 24-24h348c13.2 0 24 10.8 24 24v159.5c0 13.2-10.8 24-24 24zM380.4 89.8H127.8c-7.7 0-14-6.3-14-14v-6.3c0-7.7 6.3-14 14-14h252.6c7.7 0 14 6.3 14 14v6.3c0 7.7-6.3 14-14 14zm19.4 61.8H110.6c-7.7 0-14-6.3-14-14v-6.3c0-7.7 6.3-14 14-14h289.2c7.7 0 14 6.3 14 14v6.3c0 7.7-6.3 14-14 14zm21.6 61.4H94.6c-7.7 0-14-6.3-14-14v-6.3c0-7.7 6.3-14 14-14h326.8c7.7 0 14 6.3 14 14v6.3c0 7.7-6.3 14-14 14z"/></svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -33,36 +33,40 @@ app.config(function ($provide, $routeProvider, $interpolateProvider, $httpProvid
}); });
markdownItConverterProvider.use(markdownitLinkTarget); markdownItConverterProvider.use(markdownitLinkTarget);
$urlRouterProvider.otherwise("/"); $urlRouterProvider.otherwise('/');
$stateProvider $stateProvider
.state('list', { .state('list', {
url: "/", url: '/:filter',
templateUrl: "/boardlist.mainView.html", templateUrl: '/boardlist.mainView.html',
controller: 'ListController' controller: 'ListController',
reloadOnSearch: false,
params: {
filter: { value: '', dynamic: true }
}
}) })
.state('board', { .state('board', {
url: "/board/:boardId/:filter", url: '/board/:boardId/:filter',
templateUrl: "/board.html", templateUrl: '/board.html',
controller: 'BoardController', controller: 'BoardController',
params: { params: {
filter: { value: '', dynamic: true } filter: { value: '', dynamic: true }
} }
}) })
.state('board.detail', { .state('board.detail', {
url: "/detail/", url: '/detail/',
reloadOnSearch : false, reloadOnSearch : false,
views: { views: {
"sidebarView": { 'sidebarView': {
templateUrl: "/board.sidebarView.html" templateUrl: '/board.sidebarView.html'
} }
} }
}) })
.state('board.card', { .state('board.card', {
url: "/card/:cardId", url: '/card/:cardId',
views: { views: {
"sidebarView": { 'sidebarView': {
templateUrl: "/card.sidebarView.html", templateUrl: '/card.sidebarView.html',
controller: 'CardController' controller: 'CardController'
} }
} }

View File

@@ -20,11 +20,17 @@
* *
*/ */
app.run(function ($document, $rootScope, $transitions) { app.run(function ($document, $rootScope, $transitions, BoardService) {
'use strict'; 'use strict';
$document.click(function (event) { $document.click(function (event) {
$rootScope.$broadcast('documentClicked', 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$) { $transitions.onEnter({to: 'board.card'}, function ($state, $transition$) {
$rootScope.sidebar.show = true; $rootScope.sidebar.show = true;
}); });
@@ -40,9 +46,6 @@ app.run(function ($document, $rootScope, $transitions) {
$transitions.onExit({from: 'board.detail'}, function ($state) { $transitions.onExit({from: 'board.detail'}, function ($state) {
$rootScope.sidebar.show = false; $rootScope.sidebar.show = false;
}); });
$transitions.onEnter({to: 'board.archive'}, function ($state) {
//BoardController.loadArchived();
});
$('link[rel="shortcut icon"]').attr( $('link[rel="shortcut icon"]').attr(
'href', 'href',

View File

@@ -80,7 +80,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
} }
}; };
$scope.checkCanEdit = function () { $scope.checkCanEdit = function () {
return !$scope.archived; return !BoardService.getCurrent().archived;
}; };
// filter cards here, as ng-sortable will not work nicely with html-inline filters // 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 // Handle initial Loading
BoardService.fetchOne($scope.id).then(function (data) { BoardService.fetchOne($scope.id).then(function (data) {
BoardService.getPermissions();
$scope.statusservice.releaseWaiting(); $scope.statusservice.releaseWaiting();
}, function (error) { }, function (error) {
$scope.statusservice.setError('Error occured', error); $scope.statusservice.setError('Error occured', error);

View File

@@ -48,9 +48,10 @@ app.controller('CardController', function ($scope, $rootScope, $routeParams, $lo
} }
}; };
$scope.cardEditDescriptionShow = function($event) { $scope.cardEditDescriptionShow = function($event) {
if(BoardService.isArchived() || CardService.getCurrent().archived) {
return false;
}
var node = $event.target.nodeName; var node = $event.target.nodeName;
console.log($event);
console.log(BoardService);
if($scope.card.archived || !$scope.boardservice.canEdit()) { if($scope.card.archived || !$scope.boardservice.canEdit()) {
console.log(node); console.log(node);
} else { } else {

View File

@@ -1,4 +1,3 @@
/* /*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
* *
@@ -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.boards = [];
$scope.newBoard = {}; $scope.newBoard = {};
$scope.status = { $scope.status = {
deleteUndo: [] deleteUndo: [],
filter: $stateParams.filter ? $stateParams.filter : '',
sidebar: false
}; };
$scope.colors = ['0082c9', '00c9c6','00c906', 'c92b00', 'F1DB50', '7C31CC', '3A3B3D', 'CACBCD']; $scope.colors = ['0082c9', '00c9c6','00c906', 'c92b00', 'F1DB50', '7C31CC', '3A3B3D', 'CACBCD'];
$scope.boardservice = BoardService; $scope.boardservice = BoardService;
@@ -42,13 +45,42 @@ app.controller('ListController', function ($scope, $location, $filter, BoardServ
$scope.filterData = function () { $scope.filterData = function () {
angular.copy($scope.boardservice.getData(), $scope.boardservice.sorted); 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.selectColor = function(color) {
$scope.newBoard.color = 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() { $scope.boardCreate = function() {
if(!$scope.newBoard.title || !$scope.newBoard.color) { if(!$scope.newBoard.title || !$scope.newBoard.color) {
$scope.status.addBoard=false; $scope.status.addBoard=false;
@@ -72,26 +104,30 @@ app.controller('ListController', function ($scope, $location, $filter, BoardServ
board.status.edit = false; 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) { $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) { BoardService.delete(board.id).then(function (data) {
$scope.filterData(); $scope.filterData();
}); });
}
};
$timeout($scope.boardDeleteCountdown, 1000);
}; };
$scope.boardDeleteUndo = function (board) { $scope.boardDeleteUndo = function (board) {
delete $scope.status.deleteUndo[board.id]; BoardService.deleteUndo(board.id).then(function (data) {
$scope.filterData();
});
}; };
}); });

View File

@@ -0,0 +1,33 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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;
};
});

View File

@@ -24,15 +24,16 @@
app.filter('cardFilter', function() { app.filter('cardFilter', function() {
return function(cards, rules) { return function(cards, rules) {
var _result = {}; var _result = [];
angular.forEach(cards, function(card){ angular.forEach(cards, function(card){
var _card = card; var _card = card;
angular.some(rules, function(rule, condition) { var keys = Object.keys(rules);
if(_card[rule]===condition) { keys.some(function(key, condition) {
if(_card[key]===rules[key]) {
_result.push(_card); _result.push(_card);
} }
}); });
}); });
return result; return _result;
}; };
}); });

View File

@@ -24,7 +24,7 @@ app.filter('textColorFilter', function() {
return function (hex) { return function (hex) {
// RGB2HLS by Garry Tan // RGB2HLS by Garry Tan
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c // 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 ? { var color = result ? {
r: parseInt(result[1], 16), r: parseInt(result[1], 16),
g: parseInt(result[2], 16), g: parseInt(result[2], 16),

View File

@@ -145,6 +145,12 @@ app.factory('ApiService', function($http, $q){
return this.data[this.id]; return this.data[this.id];
}; };
ApiService.prototype.unsetCurrrent = function () {
this.id = null;
};
ApiService.prototype.getData = function() { ApiService.prototype.getData = function() {
return $.map(this.data, function(value, index) { return $.map(this.data, function(value, index) {
return [value]; return [value];

View File

@@ -26,6 +26,33 @@ app.factory('BoardService', function(ApiService, $http, $q){
}; };
BoardService.prototype = angular.copy(ApiService.prototype); 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) { BoardService.prototype.searchUsers = function (search) {
var deferred = $q.defer(); var deferred = $q.defer();
var self = this; var self = this;
@@ -151,44 +178,43 @@ app.factory('BoardService', function(ApiService, $http, $q){
return deferred.promise; 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() { BoardService.prototype.canRead = function() {
if(!this.getCurrent() || !this.getCurrent().permissions) { if(!this.getCurrent() || !this.getCurrent().permissions) {
return false; return false;
} }
return this.getCurrent().permissions['PERMISSION_READ']; return this.getCurrent().permissions['PERMISSION_READ'];
} };
BoardService.prototype.canEdit = function() { BoardService.prototype.canEdit = function() {
if(!this.getCurrent() || !this.getCurrent().permissions) { if(!this.getCurrent() || !this.getCurrent().permissions) {
return false; return false;
} }
return this.getCurrent().permissions['PERMISSION_EDIT']; 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) { if(!this.getCurrent() || !this.getCurrent().permissions) {
return false; return false;
} }
return this.getCurrent().permissions['PERMISSION_MANAGE']; return this.getCurrent().permissions['PERMISSION_MANAGE'];
} };
BoardService.prototype.canShare = function() { BoardService.prototype.canShare = function() {
if(!this.getCurrent() || !this.getCurrent().permissions) { if(!this.getCurrent() || !this.getCurrent().permissions) {
return false; return false;
} }
return this.getCurrent().permissions['PERMISSION_SHARE']; 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); service = new BoardService($http, 'boards', $q);
return service; return service;

View File

@@ -23,12 +23,12 @@
namespace OCA\Deck; namespace OCA\Deck;
class CardArchivedException extends \Exception { class ArchivedItemException extends \Exception {
/** /**
* Constructor * Constructor
* @param string $msg the error message * @param string $msg the error message
*/ */
public function __construct($msg = "") { public function __construct($msg = "Operation not allowed. Item is archived.") {
parent::__construct($msg); parent::__construct($msg);
} }
} }

View File

@@ -96,8 +96,8 @@ class BoardController extends Controller {
* @param $color * @param $color
* @return \OCP\AppFramework\Db\Entity * @return \OCP\AppFramework\Db\Entity
*/ */
public function update($id, $title, $color) { public function update($id, $title, $color, $archived) {
return $this->boardService->update($id, $title, $color); return $this->boardService->update($id, $title, $color, $archived);
} }
/** /**
@@ -108,6 +108,14 @@ class BoardController extends Controller {
public function delete($boardId) { public function delete($boardId) {
return $this->boardService->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 * @NoAdminRequired

49
lib/Cron/DeleteCron.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
/**
* 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);
}
}
}

View File

@@ -34,15 +34,19 @@ class Board extends RelationalEntity implements JsonSerializable {
protected $archived = false; protected $archived = false;
protected $labels = []; protected $labels = [];
protected $acl = []; protected $acl = [];
protected $permissions = [];
protected $shared; protected $shared;
protected $deletedAt = 0;
public function __construct() { public function __construct() {
$this->addType('id', 'integer'); $this->addType('id', 'integer');
$this->addType('shared', 'integer'); $this->addType('shared', 'integer');
$this->addType('archived', 'boolean'); $this->addType('archived', 'boolean');
$this->addType('deletedAt', 'integer');
$this->addRelation('labels'); $this->addRelation('labels');
$this->addRelation('acl'); $this->addRelation('acl');
$this->addRelation('shared'); $this->addRelation('shared');
$this->addRelation('permissions');
$this->addResolvable('owner'); $this->addResolvable('owner');
$this->shared = -1; $this->shared = -1;
} }

View File

@@ -59,7 +59,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
* @return \OCP\AppFramework\Db\Entity if not found * @return \OCP\AppFramework\Db\Entity if not found
*/ */
public function find($id, $withLabels = false, $withAcl = false) { 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` = ?'; 'WHERE `id` = ?';
$board = $this->findEntity($sql, [$id]); $board = $this->findEntity($sql, [$id]);
@@ -87,8 +87,8 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
* @return array * @return array
*/ */
public function findAllByUser($userId, $limit = null, $offset = null) { 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 ' . $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, 1 as shared FROM `*PREFIX*deck_boards` as boards ' . '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 != ?'; '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); $entries = $this->findEntities($sql, [$userId, $userId, Acl::PERMISSION_TYPE_USER, $userId], $limit, $offset);
/* @var Board $entry */ /* @var Board $entry */
@@ -112,7 +112,7 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
if (count($groups) <= 0) { if (count($groups) <= 0) {
return []; 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 ('; '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++) { for ($i = 0; $i < count($groups); $i++) {
$sql .= 'acl.participant = ? '; $sql .= 'acl.participant = ? ';
@@ -135,6 +135,15 @@ class BoardMapper extends DeckMapper implements IPermissionMapper {
return $this->findEntities($sql, []); 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 */ public function delete(/** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
\OCP\AppFramework\Db\Entity $entity) { \OCP\AppFramework\Db\Entity $entity) {
// delete acl // delete acl

View File

@@ -23,9 +23,12 @@
namespace OCA\Deck\Service; namespace OCA\Deck\Service;
use OCA\Deck\ArchivedItemException;
use OCA\Deck\Db\Acl; use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\Label; use OCA\Deck\Db\Label;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IL10N; use OCP\IL10N;
use OCA\Deck\Db\Board; use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\BoardMapper;
@@ -61,6 +64,13 @@ class BoardService {
$this->boardMapper->mapAcl($acl); $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; $result[$item->getId()] = $item;
} }
} }
@@ -77,9 +87,50 @@ class BoardService {
$this->boardMapper->mapAcl($acl); $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; 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) { public function create($title, $userId, $color) {
@@ -106,20 +157,44 @@ class BoardService {
} }
$new_board->setLabels($labels); $new_board->setLabels($labels);
$this->boardMapper->mapOwner($new_board); $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; return $new_board;
} }
public function delete($id) { public function delete($id) {
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ); $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); $this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
$board = $this->find($id); $board = $this->find($id);
$board->setTitle($title); $board->setTitle($title);
$board->setColor($color); $board->setColor($color);
$board->setArchived($archived);
$this->boardMapper->mapOwner($board); $this->boardMapper->mapOwner($board);
return $this->boardMapper->update($board); return $this->boardMapper->update($board);
} }

View File

@@ -26,18 +26,22 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Card; use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Acl; use OCA\Deck\Db\Acl;
use OCA\Deck\CardArchivedException;
use OCA\Deck\Db\StackMapper; use OCA\Deck\Db\StackMapper;
use OCA\Deck\StatusException;
class CardService { class CardService {
private $cardMapper; 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->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper; $this->stackMapper = $stackMapper;
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
$this->boardService = $boardService;
} }
public function find($cardId) { public function find($cardId) {
@@ -51,6 +55,9 @@ class CardService {
*/ */
public function create($title, $stackId, $type, $order, $owner) { public function create($title, $stackId, $type, $order, $owner) {
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT); $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 = new Card();
$card->setTitle($title); $card->setTitle($title);
$card->setStackId($stackId); $card->setStackId($stackId);
@@ -63,14 +70,20 @@ class CardService {
public function delete($id) { public function delete($id) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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)); return $this->cardMapper->delete($this->cardMapper->find($id));
} }
public function update($id, $title, $stackId, $type, $order, $description, $owner) { public function update($id, $title, $stackId, $type, $order, $description, $owner) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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 = $this->cardMapper->find($id);
if ($card->getArchived()) { if ($card->getArchived()) {
throw new CardArchivedException(); throw new StatusException('Operation not allowed. This card is archived.');
} }
$card->setTitle($title); $card->setTitle($title);
$card->setStackId($stackId); $card->setStackId($stackId);
@@ -83,9 +96,12 @@ class CardService {
public function rename($id, $title) { public function rename($id, $title) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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 = $this->cardMapper->find($id);
if ($card->getArchived()) { if ($card->getArchived()) {
throw new CardArchivedException(); throw new StatusException('Operation not allowed. This card is archived.');
} }
$card->setTitle($title); $card->setTitle($title);
return $this->cardMapper->update($card); return $this->cardMapper->update($card);
@@ -93,12 +109,15 @@ class CardService {
public function reorder($id, $stackId, $order) { public function reorder($id, $stackId, $order) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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); $cards = $this->cardMapper->findAll($stackId);
$result = []; $result = [];
$i = 0; $i = 0;
foreach ($cards as $card) { foreach ($cards as $card) {
if ($card->getArchived()) { if ($card->getArchived()) {
throw new CardArchivedException(); throw new StatusException('Operation not allowed. This card is archived.');
} }
if ($card->id === $id) { if ($card->id === $id) {
$card->setOrder($order); $card->setOrder($order);
@@ -121,6 +140,9 @@ class CardService {
public function archive($id) { public function archive($id) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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 = $this->cardMapper->find($id);
$card->setArchived(true); $card->setArchived(true);
return $this->cardMapper->update($card); return $this->cardMapper->update($card);
@@ -128,6 +150,9 @@ class CardService {
public function unarchive($id) { public function unarchive($id) {
$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT); $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 = $this->cardMapper->find($id);
$card->setArchived(false); $card->setArchived(false);
return $this->cardMapper->update($card); return $this->cardMapper->update($card);
@@ -135,18 +160,24 @@ class CardService {
public function assignLabel($cardId, $labelId) { public function assignLabel($cardId, $labelId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); $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); $card = $this->cardMapper->find($cardId);
if ($card->getArchived()) { if ($card->getArchived()) {
throw new CardArchivedException(); throw new StatusException('Operation not allowed. This card is archived.');
} }
$this->cardMapper->assignLabel($cardId, $labelId); $this->cardMapper->assignLabel($cardId, $labelId);
} }
public function removeLabel($cardId, $labelId) { public function removeLabel($cardId, $labelId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); $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); $card = $this->cardMapper->find($cardId);
if ($card->getArchived()) { if ($card->getArchived()) {
throw new CardArchivedException(); throw new StatusException('Operation not allowed. This card is archived.');
} }
$this->cardMapper->removeLabel($cardId, $labelId); $this->cardMapper->removeLabel($cardId, $labelId);
} }

View File

@@ -26,15 +26,22 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Label; use OCA\Deck\Db\Label;
use OCA\Deck\Db\Acl; use OCA\Deck\Db\Acl;
use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\LabelMapper;
use OCA\Deck\StatusException;
class LabelService { class LabelService {
/** @var LabelMapper */
private $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->labelMapper = $labelMapper;
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
$this->boardService = $boardService;
} }
public function find($labelId) { public function find($labelId) {
@@ -44,6 +51,9 @@ class LabelService {
public function create($title, $color, $boardId) { public function create($title, $color, $boardId) {
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE); $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 = new Label();
$label->setTitle($title); $label->setTitle($title);
$label->setColor($color); $label->setColor($color);
@@ -53,11 +63,17 @@ class LabelService {
public function delete($id) { public function delete($id) {
$this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE); $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)); return $this->labelMapper->delete($this->find($id));
} }
public function update($id, $title, $color) { public function update($id, $title, $color) {
$this->permissionService->checkPermission($this->labelMapper, $id, Acl::PERMISSION_MANAGE); $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 = $this->find($id);
$label->setTitle($title); $label->setTitle($title);
$label->setColor($color); $label->setColor($color);

View File

@@ -25,6 +25,7 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Acl; use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\IPermissionMapper; use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\NoPermissionException; 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 * @param $boardId
* @return bool|array * @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 * check permissions for replacing dark magic middleware
* *

View File

@@ -31,6 +31,7 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack; use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper; use OCA\Deck\Db\StackMapper;
use OCA\Deck\StatusException;
class StackService { class StackService {
@@ -39,12 +40,14 @@ class StackService {
private $cardMapper; private $cardMapper;
private $labelMapper; private $labelMapper;
private $permissionService; 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->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper; $this->cardMapper = $cardMapper;
$this->labelMapper = $labelMapper; $this->labelMapper = $labelMapper;
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
$this->boardService = $boardService;
} }
public function findAll($boardId) { public function findAll($boardId) {
@@ -81,6 +84,9 @@ class StackService {
public function create($title, $boardId, $order) { public function create($title, $boardId, $order) {
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE); $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 = new Stack();
$stack->setTitle($title); $stack->setTitle($title);
$stack->setBoardId($boardId); $stack->setBoardId($boardId);
@@ -96,6 +102,9 @@ class StackService {
public function update($id, $title, $boardId, $order) { public function update($id, $title, $boardId, $order) {
$this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE); $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 = $this->stackMapper->find($id);
$stack->setTitle($title); $stack->setTitle($title);
$stack->setBoardId($boardId); $stack->setBoardId($boardId);

View File

@@ -45,7 +45,7 @@ if(!\OC::$server->getConfig()->getSystemValue('debug', false)) {
'app' => ['App', 'Config', 'Run'], 'app' => ['App', 'Config', 'Run'],
'controller' => ['AppController', 'BoardController', 'CardController', 'ListController'], 'controller' => ['AppController', 'BoardController', 'CardController', 'ListController'],
'directive' => ['appnavigationentryutils', 'appPopoverMenuUtils', 'autofocusoninsert', 'avatar', 'elastic', 'search'], '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'], 'service' => ['ApiService', 'BoardService', 'CardService', 'LabelService', 'StackService', 'StatusService'],
]; ];
foreach($js as $folder=>$files) { foreach($js as $folder=>$files) {
@@ -58,7 +58,7 @@ if(!\OC::$server->getConfig()->getSystemValue('debug', false)) {
<div id="app" class="app-deck" data-ng-app="Deck" ng-controller="AppController" ng-cloak> <div id="app" class="app-deck" data-ng-app="Deck" ng-controller="AppController" ng-cloak>
<div id="app-navigation" data-ng-controller="ListController"> <div id="app-navigation" data-ng-controller="ListController" ng-init="initSidebar()">
<?php print_unescaped($this->inc('part.navigation')); ?> <?php print_unescaped($this->inc('part.navigation')); ?>
<?php /* print_unescaped($this->inc('part.settings')); */ ?> <?php /* print_unescaped($this->inc('part.settings')); */ ?>
</div> </div>

View File

@@ -65,12 +65,9 @@
</div> </div>
<div class="card-assignees" ng-if="c.assignees">
<!-- <div class="avatar" avatar user="{{c.owner}}" size="24"></div>//-->
</div>
<div class="card-controls"> <div class="card-controls">
<i class="icon icon-filetype-text" ng-if="c.description" title="{{ c.description }}"></i> <i class="icon icon-filetype-text" ng-if="c.description" title="{{ c.description }}"></i>
<div class="app-popover-menu-utils"> <div class="app-popover-menu-utils" ng-if="!boardservice.isArchived()">
<button class="button-inline card-options icon-more" ng-model="card"></button> <button class="button-inline card-options icon-more" ng-model="card"></button>
<div class="popovermenu hidden"> <div class="popovermenu hidden">
<ul> <ul>
@@ -96,14 +93,11 @@
</div> </div>
</div> </div>
</div> </div>
<!--<span class="info due"><i class="fa fa-clock-o" aria-hidden="true"></i> <span>Today</span></span>
<span class="info tasks"><i class="fa fa-list" aria-hidden="true"></i> <span>3/12</span></span>
//-->
</div> </div>
</li> </li>
</ul> </ul>
<!-- CREATE CARD //--> <!-- CREATE CARD //-->
<div class="card create" <div class="card create"
style="background-color:#{{ boardservice.getCurrent().color }};" ng-if="boardservice.canEdit() && checkCanEdit() && filter!=='archive'"> style="background-color:#{{ boardservice.getCurrent().color }};" ng-if="boardservice.canEdit() && checkCanEdit() && filter!=='archive'">

View File

@@ -9,6 +9,7 @@
<h2>{{ boardservice.getCurrent().title }}</h2> <h2>{{ boardservice.getCurrent().title }}</h2>
</div> </div>
{{board=boardservice.getCurrent();""}}
<ul class="tabHeaders"> <ul class="tabHeaders">
<li class="tabHeader" ng-class="{'selected': (status.boardtab==0 || !status.boardtab)}" ng-click="status.boardtab=0"><a><?php p($l->t('Sharing')); ?></a></li> <li class="tabHeader" ng-class="{'selected': (status.boardtab==0 || !status.boardtab)}" ng-click="status.boardtab=0"><a><?php p($l->t('Sharing')); ?></a></li>
@@ -90,7 +91,7 @@
<input type="text" class="input-inline" ng-model="newLabel.title" style="color:{{ newLabel.color|textColorFilter }};" autofocus-on-insert maxlength="100" /> <input type="text" class="input-inline" ng-model="newLabel.title" style="color:{{ newLabel.color|textColorFilter }};" autofocus-on-insert maxlength="100" />
</span> </span>
<div class="colorselect"> <div class="colorselect">
<div class="color" ng-repeat="c in defaultColors" style="background-color:#{{ c }};" ng-click="newLabel.color=c" ng-class="{'selected': (c == newLabel.color) }"><br /></div> <div class="color" ng-repeat="c in defaultColors" style="background-color:#{{ c }};" ng-click="newLabel.color=c" ng-class="{'selected': (c == newLabel.color), 'dark': (newBoard.color | textColorFilter) === '#ffffff' }"><br /></div>
</div> </div>
<a ng-click="labelCreate(newLabel)" class="icon"><i class="icon icon-checkmark" ></i></a> <a ng-click="labelCreate(newLabel)" class="icon"><i class="icon icon-checkmark" ></i></a>
<a ng-click="status.createLabel=false" class="icon icon-close"></a> <a ng-click="status.createLabel=false" class="icon icon-close"></a>

View File

@@ -5,31 +5,66 @@
<td class="cell-board-bullet"></td> <td class="cell-board-bullet"></td>
<td class="cell-board-title" width="90%"><?php p($l->t('Title')); ?></td> <td class="cell-board-title" width="90%"><?php p($l->t('Title')); ?></td>
<td class="cell-board-members"><?php p($l->t('Members')); ?></td> <td class="cell-board-members"><?php p($l->t('Members')); ?></td>
<td></td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr data-ng-repeat="b in boardservice.sorted" <tr data-ng-repeat="b in boardservice.sorted" ng-class="{deleted: b.deletedAt > 0}">
ui-sref="board({boardId: b.id})"> <td ng-click="gotoBoard(b)">
<td>
<span class="board-bullet" <span class="board-bullet"
style="background-color:#{{b.color}};"> </span> style="background-color:#{{b.color}};"> </span>
</td> </td>
<td><a href="#/board/{{b.id}}">{{ b.title }}</a></td> <td ng-click="gotoBoard(b)">{{ b.title }}</a></td>
<td> <td>
<div id="assigned-users"> <div id="assigned-users">
<div class="avatardiv" avatar displayname="{{ b.owner.uid }}" title="{{ b.owner.displayname }}"></div> <div class="avatardiv" avatar displayname="{{ b.owner.uid }}" title="{{ b.owner.displayname }}"></div>
<div class="avatardiv" avatar displayname="{{ acl.participant.uid }}" title="{{ acl.participant.uid }}" ng-repeat="acl in b.acl | limitTo: 7"></div> <div class="avatardiv" avatar displayname="{{ acl.participant.uid }}" title="{{ acl.participant.uid }}" ng-repeat="acl in b.acl | limitTo: 7"></div>
</div> </div>
</td> </td>
</tr>
<tr>
<td><span class="icon icon-add"></span></td>
<td> <td>
<div class="hint"></div>
<div class="app-popover-menu-utils" ng-if="b.deletedAt == 0">
<button class="icon icon-more button-inline" title="More actions"></button>
<div class="popovermenu bubble hidden">
<ul>
<li ng-if="boardservice.canManage(b) && !b.archived" ng-click="boardArchive(b)">
<a class="menuitem"><span class="icon-archive"></span> <?php p($l->t('Archive board')); ?>
</a>
</li>
<li ng-if="boardservice.canManage(b) && b.archived" ng-click="boardUnarchive(b)">
<a class="menuitem"><span class="icon-archive"></span> <?php p($l->t('Unarchive board')); ?>
</a>
</li>
<li ng-if="boardservice.canManage(b)" ng-click="boardDelete(b)">
<a class="menuitem"><span class="icon-delete"></span> <?php p($l->t('Delete board')); ?>
</a>
</li>
<li ui-sref="board.detail({boardId: b.id})">
<a class="menuitem"><span class="icon-info"></span> <?php p($l->t('Board details')); ?>
</a>
</li>
</ul>
</div>
</div>
<div class="app-popover-menu-utils" ng-if="b.deletedAt > 0">
<button class="icon icon-history button-inline" ng-click="boardDeleteUndo(b)" title="Undo board deletion - Otherwise the board will be deleted during the next cronjob run."></button>
</div>
</td>
</tr>
<tr ng-if="status.filter === '' && !status.addBoard" ng-click="status.addBoard=!status.addBoard">
<td><span class="icon icon-add"></span></td>
<td colspan="3">
<a ng-click="status.addBoard=!status.addBoard" <a ng-click="status.addBoard=!status.addBoard"
ng-show="!status.addBoard"> ng-show="!status.addBoard">
<?php p($l->t('Create new board')); ?> <?php p($l->t('Create new board')); ?>
</a> </a>
<form ng-show="status.addBoard" ng-disabled="isAddingList" </td>
</tr>
<tr ng-if="status.filter === '' && status.addBoard">
<td><span class="icon icon-add"></span></td>
<td>
<form ng-disabled="isAddingList"
class="ng-pristine ng-valid" ng-submit="boardCreate()"> class="ng-pristine ng-valid" ng-submit="boardCreate()">
<input id="newTitle" class="edit ng-valid ng-empty" <input id="newTitle" class="edit ng-valid ng-empty"
type="text" placeholder="<?php p($l->t('New board title')); ?>" type="text" placeholder="<?php p($l->t('New board title')); ?>"
@@ -38,13 +73,13 @@
<div class="color" ng-repeat="c in colors" <div class="color" ng-repeat="c in colors"
style="background-color:#{{ c }};" style="background-color:#{{ c }};"
ng-click="selectColor(c)" ng-click="selectColor(c)"
ng-class="{'selected': (c == newBoard.color) }"> ng-class="{'selected': (c == newBoard.color), 'dark': (newBoard.color | textColorFilter) === '#ffffff' }"></div>
<br/></div>
</div> </div>
<input type="submit" value="" class="icon-checkmark svg"> <input type="submit" value="" class="icon-checkmark svg" />
</form> </form>
</td> </td>
<td></td> <td></td>
<td></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -31,6 +31,7 @@
<div id="labels"> <div id="labels">
<ui-select multiple tagging="" ng-model="card.labels" theme="select2" <ui-select multiple tagging="" ng-model="card.labels" theme="select2"
ng-disabled="boardservice.isArchived() || card.archived"
style="width:100%;" title="Choose a label" style="width:100%;" title="Choose a label"
placeholder="Add a label" placeholder="Add a label"
on-select="labelAssign($item, $model)" on-select="labelAssign($item, $model)"

View File

@@ -1,28 +1,24 @@
<ul class="with-icon"> <ul class="with-icon">
<li><a href="#" class=""><?php p($l->t('All Boards')); ?></a></li> <li ng-class="{active: status.filter === '' && !boardservice.getCurrent()}"><a ui-sref="list({ filter: ''})" class="icon-deck"><?php p($l->t('All Boards')); ?></a></li>
<li ng-class="{active: status.filter === 'archived' || (boardservice.getCurrent() && boardservice.getCurrent().archived)}"><a ui-sref="list({ filter: 'archived' })" class="icon-archive"><?php p($l->t('Archived boards')); ?></a></li>
<li ng-class="{active: status.filter === 'shared'}"><a ui-sref="list({ filter: 'shared' })" class="icon-share"><?php p($l->t('Shared boards')); ?></a></li>
<li class="with-icon with-menu" ng-class="{active: b.id === boardservice.getCurrent().id}" data-ng-repeat="b in boardservice.sorted"> <li class="with-icon with-menu" ng-class="{active: b.id === boardservice.getCurrent().id}" data-ng-repeat="b in boardservice.sidebar" ng-if="b.deletedAt == 0">
<span class="board-bullet" style="background-color:#{{b.color}};" ng-if="!b.status.edit"> </span> <span class="board-bullet" style="background-color:#{{b.color}};" ng-if="!b.status.edit"> </span>
<a href="#!/board/{{b.id}}/" ng-if="!b.status.edit">{{ b.title }}</a> <a href="#!/board/{{b.id}}/" ng-if="!b.status.edit">{{ b.title }}</a>
<div class="app-navigation-entry-utils" ng-show="!b.status.edit" style="position:absolute;"> <div class="app-navigation-entry-utils" ng-show="!b.status.edit" style="position:absolute;">
<ul> <ul>
<li class="app-navigation-entry-utils-counter board-delete-undo" ng-show="status.deleteUndo[b.id]" ng-click="boardDeleteUndo(b)" title="Click to undo">Deleting in {{ status.deleteUndo[b.id] }}s &nbsp; X</li>
<li class="app-navigation-entry-utils-menu-share svg" ng-show="b.shared>0"><i class="icon icon-share" title="<?php p($l->t('Shared with you')); ?>"> </i></li> <li class="app-navigation-entry-utils-menu-share svg" ng-show="b.shared>0"><i class="icon icon-share" title="<?php p($l->t('Shared with you')); ?>"> </i></li>
<li class="app-navigation-entry-utils-menu-button svg" ng-show="!status.deleteUndo[b.id]"><button class="icon-more"></button></li> <li class="app-navigation-entry-utils-menu-button svg" ng-show="!status.deleteUndo[b.id]"><button class="icon-more"></button></li>
</ul> </ul>
</div> </div>
<div class="app-navigation-entry-menu app-navigation-noclose" ng-show="!b.status.edit"> <div class="app-navigation-entry-menu app-navigation-noclose" ng-show="!b.status.edit">
<ul> <ul>
<li ng-show="b.owner.uid===user"><button class="icon-rename svg" title="<?php p($l->t('edit')); ?>" ng-click="b.status.edit=true"></button></li> <li ng-show="boardservice.canManage(b)"><button class="icon-rename svg" title="<?php p($l->t('Edit board')); ?>" ng-click="b.status.edit=true"></button></li>
<li ng-show="b.owner.uid===user"><button class="icon-delete svg" title="<?php p($l->t('delete')); ?>" ng-click="boardDelete(b)"></button></li> <li ng-show="boardservice.canManage(b)"><button class="icon-archive svg" title="<?php p($l->t('Move board to archive')); ?>" ng-click="boardArchive(b)"></button></li>
<li ng-show="b.owner.uid!==user && false"><button class="icon-delete svg" title="<?php p($l->t('remove share')); ?>" ng-click="boardRemoveShare(b)"></button></li>
</ul> </ul>
</div> </div>
<div class="app-navigation-entry-deleted" ng-show="false">
<div class="app-navigation-entry-deleted-description">Deleted X</div>
<button class="app-navigation-entry-deleted-button icon-history svg" title="Undo"></button>
</div>
<div class="app-navigation-entry-edit" ng-show="b.status.edit"> <div class="app-navigation-entry-edit" ng-show="b.status.edit">
<form ng-disabled="isAddingList" class="ng-pristine ng-valid" ng-submit="boardUpdate(b)"> <form ng-disabled="isAddingList" class="ng-pristine ng-valid" ng-submit="boardUpdate(b)">
<input id="newTitle" class="edit ng-valid ng-empty" type="text" autofocus-on-insert ng-model="b.title" maxlength="100"> <input id="newTitle" class="edit ng-valid ng-empty" type="text" autofocus-on-insert ng-model="b.title" maxlength="100">
@@ -43,7 +39,7 @@
<input id="newTitle" class="edit ng-valid ng-empty" type="text" placeholder="<?php p($l->t('New board title')); ?>" autofocus-on-insert ng-model="newBoard.title" maxlength="100"> <input id="newTitle" class="edit ng-valid ng-empty" type="text" placeholder="<?php p($l->t('New board title')); ?>" autofocus-on-insert ng-model="newBoard.title" maxlength="100">
<input type="submit" value="" class="action icon-checkmark svg"> <input type="submit" value="" class="action icon-checkmark svg">
<div class="colorselect"> <div class="colorselect">
<div class="color" ng-repeat="c in colors" style="background-color:#{{ c }};" ng-click="selectColor(c)" ng-class="{'selected': (c == newBoard.color) }"><br /></div> <div class="color" ng-repeat="c in colors" style="background-color:#{{ c }};" ng-click="selectColor(c)" ng-class="{'selected': (c == newBoard.color), 'dark': (newBoard.color | textColorFilter) === '#ffffff' }"><br /></div>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -24,7 +24,7 @@
/** /**
* @group DB * @group DB
*/ */
class BoardDatabaseTest extends \PHPUnit_Framework_TestCase class BoardDatabaseTest extends \Test\TestCase
{ {
const TEST_USER1 = "test-share-user1"; const TEST_USER1 = "test-share-user1";
const TEST_USER2 = "test-share-user2"; const TEST_USER2 = "test-share-user2";
@@ -61,6 +61,7 @@ class BoardDatabaseTest extends \PHPUnit_Framework_TestCase
\OC::$server->getGroupManager()->addBackend($groupBackend); \OC::$server->getGroupManager()->addBackend($groupBackend);
} }
public function setUp() { public function setUp() {
parent::setUp();
\OC::$server->getUserSession()->login(self::TEST_USER1, self::TEST_USER1); \OC::$server->getUserSession()->login(self::TEST_USER1, self::TEST_USER1);
$this->boardService = \OC::$server->query("\OCA\Deck\Service\BoardService"); $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->getTitle(), $board->getTitle());
$this->assertEquals($actual->getColor(), $board->getColor()); $this->assertEquals($actual->getColor(), $board->getColor());
$this->assertEquals($actual->getOwner(), $board->getOwner()); $this->assertEquals($actual->getOwner(), $board->getOwner());
$this->boardService->delete($id); $this->boardService->deleteForce($id);
} }
public function tearDown() { public function tearDown() {
parent::tearDown();
} }
} }

View File

@@ -0,0 +1,72 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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]);
}
}

View File

@@ -23,6 +23,7 @@
namespace OCA\Deck\Db; namespace OCA\Deck\Db;
use OCP\IDBConnection;
use OCP\IGroupManager; use OCP\IGroupManager;
use OCP\IUserManager; use OCP\IUserManager;
use Test\AppFramework\Db\MapperTestUtility; use Test\AppFramework\Db\MapperTestUtility;
@@ -32,10 +33,15 @@ use Test\AppFramework\Db\MapperTestUtility;
*/ */
class BoardMapperTest extends MapperTestUtility { class BoardMapperTest extends MapperTestUtility {
/** @var IDBConnection */
private $dbConnection; private $dbConnection;
/** @var AclMapper|\PHPUnit_Framework_MockObject_MockObject */
private $aclMapper; private $aclMapper;
/** @var BoardMapper */
private $boardMapper; private $boardMapper;
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
private $userManager; private $userManager;
/** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */
private $groupManager; private $groupManager;
// Data // Data
@@ -101,14 +107,55 @@ class BoardMapperTest extends MapperTestUtility {
public function testFind() { public function testFind() {
$actual = $this->boardMapper->find($this->boards[0]->getId()); $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); $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() { public function testFindWithLabels() {
$actual = $this->boardMapper->find($this->boards[0]->getId(), true, false); $actual = $this->boardMapper->find($this->boards[0]->getId(), true, false);
/** @var Board $expected */
$expected = $this->boards[0]; $expected = $this->boards[0];
$this->assertEquals($expected, $actual); $this->assertEquals($expected->getLabels(), $actual->getLabels());
} }
public function testFindWithAcl() { public function testFindWithAcl() {
$actual = $this->boardMapper->find($this->boards[0]->getId(), false, true); $actual = $this->boardMapper->find($this->boards[0]->getId(), false, true);
$expected = [ $expected = [

View File

@@ -2,7 +2,9 @@
namespace OCA\Deck\Db; namespace OCA\Deck\Db;
class BoardTest extends \PHPUnit_Framework_TestCase { use PHPUnit\Framework\TestCase;
class BoardTest extends TestCase {
private function createBoard() { private function createBoard() {
$board = new Board(); $board = new Board();
$board->setId(1); $board->setId(1);
@@ -21,6 +23,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase {
'owner' => "admin", 'owner' => "admin",
'color' => "000000", 'color' => "000000",
'labels' => array(), 'labels' => array(),
'permissions' => [],
'deletedAt' => 0,
'acl' => array(), 'acl' => array(),
'archived' => false 'archived' => false
], $board->jsonSerialize()); ], $board->jsonSerialize());
@@ -35,6 +39,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase {
'owner' => "admin", 'owner' => "admin",
'color' => "000000", 'color' => "000000",
'labels' => array("foo", "bar"), 'labels' => array("foo", "bar"),
'permissions' => [],
'deletedAt' => 0,
'acl' => array(), 'acl' => array(),
'archived' => false 'archived' => false
], $board->jsonSerialize()); ], $board->jsonSerialize());
@@ -56,6 +62,8 @@ class BoardTest extends \PHPUnit_Framework_TestCase {
'owner' => "admin", 'owner' => "admin",
'color' => "000000", 'color' => "000000",
'labels' => array(), 'labels' => array(),
'permissions' => [],
'deletedAt' => 0,
'acl' => array(), 'acl' => array(),
'archived' => false, 'archived' => false,
'shared' => 1, 'shared' => 1,

View File

@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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());
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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());
}
}

View File

@@ -23,7 +23,7 @@
namespace OCA\Deck\Db; namespace OCA\Deck\Db;
use OCA\Deck\CardArchivedException; use OCA\Deck\ArchivedItemException;
use OCA\Deck\Controller\PageController; use OCA\Deck\Controller\PageController;
use OCA\Deck\NoPermissionException; use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException; use OCA\Deck\NotFoundException;
@@ -45,7 +45,7 @@ class ExceptionsTest extends \PHPUnit_Framework_TestCase {
} }
public function testCardArchivedException() { public function testCardArchivedException() {
$e = new CardArchivedException('foo'); $e = new ArchivedItemException('foo');
$this->assertEquals('foo', $e->getMessage()); $this->assertEquals('foo', $e->getMessage());
} }

View File

@@ -0,0 +1,128 @@
<?php
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
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;
}
}

View File

@@ -53,6 +53,7 @@ class BoardServiceTest extends \Test\TestCase {
private $userId = 'admin'; private $userId = 'admin';
public function setUp() { public function setUp() {
parent::setUp();
$this->l10n = $this->createMock(L10N::class); $this->l10n = $this->createMock(L10N::class);
$this->aclMapper = $this->createMock(AclMapper::class); $this->aclMapper = $this->createMock(AclMapper::class);
$this->boardMapper = $this->createMock(BoardMapper::class); $this->boardMapper = $this->createMock(BoardMapper::class);
@@ -134,11 +135,12 @@ class BoardServiceTest extends \Test\TestCase {
->method('update') ->method('update')
->with($board) ->with($board)
->willReturn($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->getTitle(), 'MyNewNameBoard');
$this->assertEquals($b->getOwner(), 'admin'); $this->assertEquals($b->getOwner(), 'admin');
$this->assertEquals($b->getColor(), 'ffffff'); $this->assertEquals($b->getColor(), 'ffffff');
$this->assertEquals($b->getArchived(), false);
} }
public function testDelete() { public function testDelete() {
@@ -147,10 +149,7 @@ class BoardServiceTest extends \Test\TestCase {
$this->boardMapper->expects($this->once()) $this->boardMapper->expects($this->once())
->method('find') ->method('find')
->willReturn($board); ->willReturn($board);
$this->boardMapper->expects($this->once()) $this->assertEquals($board, $this->service->delete(123));
->method('delete')
->willReturn(1);
$this->assertEquals(1, $this->service->delete(123));
} }
public function testAddAcl() { public function testAddAcl() {

View File

@@ -27,9 +27,10 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Card; use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper; 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 */ /** @var CardService|\PHPUnit_Framework_MockObject_MockObject */
private $cardService; private $cardService;
@@ -39,16 +40,19 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
private $stackMapper; private $stackMapper;
/** @var PermissionService|\PHPUnit_Framework_MockObject_MockObject */ /** @var PermissionService|\PHPUnit_Framework_MockObject_MockObject */
private $permissionService; private $permissionService;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
private $boardService;
public function setUp() { public function setUp() {
parent::setUp();
$this->cardMapper = $this->getMockBuilder(CardMapper::class) $this->cardMapper = $this->getMockBuilder(CardMapper::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->stackMapper = $this->getMockBuilder(StackMapper::class) $this->stackMapper = $this->getMockBuilder(StackMapper::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->permissionService = $this->getMockBuilder(PermissionService::class) $this->permissionService = $this->getMockBuilder(PermissionService::class)
->disableOriginalConstructor()->getMock(); ->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() { public function testFind() {
@@ -108,7 +112,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
$card->setArchived(true); $card->setArchived(true);
$this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('find')->willReturn($card);
$this->cardMapper->expects($this->never())->method('update'); $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'); $this->cardService->update(123, 'newtitle', 234, 'text', 999, 'foo', 'admin');
} }
@@ -128,7 +132,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
$card->setArchived(true); $card->setArchived(true);
$this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('find')->willReturn($card);
$this->cardMapper->expects($this->never())->method('update'); $this->cardMapper->expects($this->never())->method('update');
$this->setExpectedException(CardArchivedException::class); $this->setExpectedException(StatusException::class);
$this->cardService->rename(123, 'newtitle'); $this->cardService->rename(123, 'newtitle');
} }
@@ -168,7 +172,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
$card->setArchived(true); $card->setArchived(true);
$this->cardMapper->expects($this->once())->method('findAll')->willReturn([$card]); $this->cardMapper->expects($this->once())->method('findAll')->willReturn([$card]);
$this->cardMapper->expects($this->never())->method('update')->willReturnCallback(function($c) { return $c; }); $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); $actual = $this->cardService->reorder(123, 234, 1);
} }
public function testArchive() { public function testArchive() {
@@ -204,7 +208,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
$card->setArchived(true); $card->setArchived(true);
$this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('find')->willReturn($card);
$this->cardMapper->expects($this->never())->method('assignLabel'); $this->cardMapper->expects($this->never())->method('assignLabel');
$this->setExpectedException(CardArchivedException::class); $this->setExpectedException(StatusException::class);
$this->cardService->assignLabel(123, 999); $this->cardService->assignLabel(123, 999);
} }
@@ -221,7 +225,7 @@ class CardServiceTest extends \PHPUnit_Framework_TestCase {
$card->setArchived(true); $card->setArchived(true);
$this->cardMapper->expects($this->once())->method('find')->willReturn($card); $this->cardMapper->expects($this->once())->method('find')->willReturn($card);
$this->cardMapper->expects($this->never())->method('removeLabel'); $this->cardMapper->expects($this->never())->method('removeLabel');
$this->setExpectedException(CardArchivedException::class); $this->setExpectedException(StatusException::class);
$this->cardService->removeLabel(123, 999); $this->cardService->removeLabel(123, 999);
} }

View File

@@ -26,8 +26,9 @@ namespace OCA\Deck\Service;
use OCA\Deck\Db\Label; use OCA\Deck\Db\Label;
use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\LabelMapper;
use Test\TestCase;
class LabelServiceTest extends \PHPUnit_Framework_TestCase { class LabelServiceTest extends TestCase {
/** @var LabelMapper|\PHPUnit_Framework_MockObject_MockObject */ /** @var LabelMapper|\PHPUnit_Framework_MockObject_MockObject */
private $labelMapper; private $labelMapper;
@@ -35,15 +36,20 @@ class LabelServiceTest extends \PHPUnit_Framework_TestCase {
private $permissionService; private $permissionService;
/** @var LabelService */ /** @var LabelService */
private $labelService; private $labelService;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
private $boardService;
public function setUp() { public function setUp() {
parent::setUp();
$this->labelMapper = $this->getMockBuilder(LabelMapper::class) $this->labelMapper = $this->getMockBuilder(LabelMapper::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->permissionService = $this->getMockBuilder(PermissionService::class) $this->permissionService = $this->getMockBuilder(PermissionService::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->boardService = $this->createMock(BoardService::class);
$this->labelService = new LabelService( $this->labelService = new LabelService(
$this->labelMapper, $this->labelMapper,
$this->permissionService $this->permissionService,
$this->boardService
); );
} }

View File

@@ -46,6 +46,7 @@ class PermissionServiceTest extends \PHPUnit_Framework_TestCase {
private $userId = 'admin'; private $userId = 'admin';
public function setUp() { public function setUp() {
parent::setUp();
$this->logger = $this->request = $this->getMockBuilder(ILogger::class) $this->logger = $this->request = $this->getMockBuilder(ILogger::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();

View File

@@ -31,8 +31,15 @@ use OCA\Deck\Db\Label;
use OCA\Deck\Db\LabelMapper; use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\Stack; use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper; 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 */ /** @var StackService */
private $stackService; private $stackService;
@@ -44,8 +51,11 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase {
private $labelMapper; private $labelMapper;
/** @var \PHPUnit_Framework_MockObject_MockObject|PermissionService */ /** @var \PHPUnit_Framework_MockObject_MockObject|PermissionService */
private $permissionService; private $permissionService;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
private $boardService;
public function setUp() { public function setUp() {
parent::setUp();
$this->stackMapper = $this->getMockBuilder(StackMapper::class) $this->stackMapper = $this->getMockBuilder(StackMapper::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->cardMapper = $this->getMockBuilder(CardMapper::class) $this->cardMapper = $this->getMockBuilder(CardMapper::class)
@@ -54,12 +64,14 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase {
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->permissionService = $this->getMockBuilder(PermissionService::class) $this->permissionService = $this->getMockBuilder(PermissionService::class)
->disableOriginalConstructor()->getMock(); ->disableOriginalConstructor()->getMock();
$this->boardService = $this->createMock(BoardService::class);
$this->stackService = new StackService( $this->stackService = new StackService(
$this->stackMapper, $this->stackMapper,
$this->cardMapper, $this->cardMapper,
$this->labelMapper, $this->labelMapper,
$this->permissionService $this->permissionService,
$this->boardService
); );
} }
@@ -160,6 +172,9 @@ class StackServiceTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals($stack, $result); $this->assertEquals($stack, $result);
} }
/**
* @group DB
*/
public function testReorder() { public function testReorder() {
$this->permissionService->expects($this->once())->method('checkPermission'); $this->permissionService->expects($this->once())->method('checkPermission');
$a = $this->createStack(1, 0); $a = $this->createStack(1, 0);

View File

@@ -107,9 +107,9 @@ class BoardControllerTest extends \PHPUnit_Framework_TestCase {
public function testUpdate() { public function testUpdate() {
$this->boardService->expects($this->once()) $this->boardService->expects($this->once())
->method('update') ->method('update')
->with(1, 2, 3) ->with(1, 2, 3, false)
->willReturn(1); ->willReturn(1);
$this->assertEquals(1, $this->controller->update(1, 2, 3)); $this->assertEquals(1, $this->controller->update(1, 2, 3, false));
} }
public function testDelete() { public function testDelete() {
@@ -120,6 +120,14 @@ class BoardControllerTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals(1, $this->controller->delete(123)); $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() { public function testGetUserPermissions() {
$acl = [ $acl = [
Acl::PERMISSION_READ => true, Acl::PERMISSION_READ => true,