Merge pull request #607 from nextcloud/feature/211/activity

Activity integration
This commit is contained in:
Julius Härtl
2018-09-07 16:18:39 +02:00
committed by GitHub
46 changed files with 2649 additions and 118 deletions

View File

@@ -134,6 +134,7 @@ pipeline:
- cd ../server/
- ./occ app:enable $APP_NAME
- cd apps/$APP_NAME
- composer install
- phpunit -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
- phpunit -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
when:
@@ -153,6 +154,7 @@ pipeline:
- php occ app:enable deck
- cd apps/$APP_NAME
# Run phpunit tests
- composer install
- phpunit -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
- phpunit -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
when:
@@ -171,6 +173,7 @@ pipeline:
- cd ../server/
- php occ app:enable deck
- cd apps/$APP_NAME
- composer install
- phpunit -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
- phpunit -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
when:
@@ -189,6 +192,7 @@ pipeline:
- cd ../server/
- php occ app:enable deck
- cd apps/$APP_NAME
- composer install
- phpunit -c tests/phpunit.xml --coverage-clover build/php-unit.coverage.xml
- phpunit -c tests/phpunit.integration.xml --coverage-clover build/php-integration.coverage.xml
when:

View File

@@ -6,6 +6,7 @@ extends:
env:
browser: true
amd: true
es6: true
globals:
global: false

View File

@@ -24,6 +24,7 @@ before_script:
- cd apps/deck
script:
- composer install
- make test-unit
after_success:

View File

@@ -20,12 +20,15 @@ clean-build:
clean-dist:
rm -rf js/node_modules
install-deps:
install-deps: install-deps-js
composer install
install-deps-js:
cd js && npm install
build: build-js
build: install-deps build-js
build-js: install-deps
build-js: install-deps-js
cd js && npm run build
build-js-dev: install-deps

View File

@@ -21,6 +21,12 @@
*
*/
if ((@include_once __DIR__ . '/../vendor/autoload.php')===false) {
throw new Exception('Cannot include autoload. Did you run install dependencies using composer?');
}
$app = new \OCA\Deck\AppInfo\Application();
$app->registerNavigationEntry();
$app->registerNotifications();
$app->registerNotifications();
\OC_Util::addStyle('deck', 'activity');

View File

@@ -5,20 +5,20 @@
* @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\AppInfo;
@@ -28,4 +28,4 @@ use OCP\AppFramework\App;
/**
* Additional autoloader registration, e.g. registering composer autoloaders
*/
// require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../vendor/autoload.php';

View File

@@ -14,7 +14,7 @@
- 🚀 Get your project organized
</description>
<version>0.5.0-dev2</version>
<version>0.5.0-dev3</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
@@ -40,4 +40,15 @@
<commands>
<command>OCA\Deck\Command\UserExport</command>
</commands>
<activity>
<settings>
<setting>OCA\Deck\Activity\Setting</setting>
</settings>
<filters>
<filter>OCA\Deck\Activity\Filter</filter>
</filters>
<providers>
<provider>OCA\Deck\Activity\DeckProvider</provider>
</providers>
</activity>
</info>

View File

@@ -8,8 +8,11 @@
"email": "jus@bitgrid.net"
}
],
"require": {},
"require": {
"cogpowered/finediff": "0.3.*"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"christophwurst/nextcloud": "^13.0",
"jakub-onderka/php-parallel-lint": "^1.0.0"
}

23
css/activity.css Normal file
View File

@@ -0,0 +1,23 @@
.activitymessage .visualdiff ins {
background-color: rgba(70, 186, 97, 0.2);
text-decoration: none;
}
.activitymessage .visualdiff del {
background-color: rgba(233, 50, 45, 0.2);
text-decoration: none;
}
.activityTabView .avatardiv-container {
display: inline-block;
bottom: -3px;
margin-left: 3px;
}
.activityTabView .avatar-name-wrapper {
font-weight: bold;
}
.activityTabView .activitysubject a {
font-weight: bold;
}

View File

@@ -38,7 +38,7 @@
}
.icon-badge {
background-image: url('../../../core/img/places/calendar-dark.svg');
background-image: url('../img/calendar-dark.svg');
}
.icon-toggle-compact-collapsed {

View File

@@ -1258,7 +1258,6 @@ input.input-inline {
.tab {
height: 100%;
overflow: scroll;
}
}

1
img/calendar-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" version="1.1" height="32" viewbox="0 0 32 32"><path fill="#000" d="m8 2c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm16 0c-1.108 0-2 0.892-2 2v4c0 1.108 0.892 2 2 2s2-0.892 2-2v-4c0-1.108-0.892-2-2-2zm-13 4v2c0 1.662-1.338 3-3 3s-3-1.338-3-3v-1.875a3.993 3.993 0 0 0 -3 3.875v16c0 2.216 1.784 4 4 4h20c2.216 0 4-1.784 4-4v-16a3.993 3.993 0 0 0 -3 -3.875v1.875c0 1.662-1.338 3-3 3s-3-1.338-3-3v-2zm-4.906 10h19.812a0.09 0.09 0 0 1 0.094 0.094v9.812a0.09 0.09 0 0 1 -0.094 0.094h-19.812a0.09 0.09 0 0 1 -0.094 -0.094v-9.812a0.09 0.09 0 0 1 0.094 -0.094z"/></svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@@ -4,20 +4,20 @@
* @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/>.
*
*
*/
/* global angular */
@@ -48,13 +48,15 @@ import ngsortable from 'ng-sortable';
import md from 'angular-markdown-it';
import nganimate from 'angular-animate';
import 'angular-file-upload';
import ngInfiniteScroll from 'ng-infinite-scroll';
var app = angular.module('Deck', [
ngsanitize,
uirouter,
angularuiselect,
ngsortable, md, nganimate,
'angularFileUpload'
'angularFileUpload',
ngInfiniteScroll
]);
export default app;

View File

@@ -0,0 +1,99 @@
/*
* @copyright Copyright (c) 2018 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/>.
*
*/
/* global OC OCA */
class ActivityController {
constructor ($scope, CardService, ActivityService) {
'ngInject';
this.cardservice = CardService;
this.activityservice = ActivityService;
this.$scope = $scope;
this.type = '';
this.loading = false;
const self = this;
this.$scope.$watch(function () {
return self.element.id;
}, function (params) {
if (self.getData(self.element.id).length === 0) {
self.loading = true;
self.fetchUntilResults();
}
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
}, true);
}
getData(id) {
return this.activityservice.getData(this.type, id);
}
parseMessage(subject, parameters) {
OCA.Activity.RichObjectStringParser._userLocalTemplate = '<span class="avatar-name-wrapper"><avatar ng-attr-contactsmenu ng-attr-tooltip ng-attr-user="{{ id }}" ng-attr-displayname="{{name}}" ng-attr-size="16"></avatar> {{ name }}</span>';
return OCA.Activity.RichObjectStringParser.parseMessage(subject, parameters);
}
fetchUntilResults () {
const self = this;
let dataLengthBefore = self.getData(self.element.id).length;
let _executeFetch = function() {
let promise = self.activityservice.fetchMoreActivities(self.type, self.element.id);
promise.then(function (data) {
let dataLengthAfter = self.getData(self.element.id).length;
if (data !== null && (dataLengthAfter <= dataLengthBefore || dataLengthAfter < 5)) {
_executeFetch();
} else {
self.loading = false;
}
}, function () {
self.loading = false;
self.$scope.$apply();
});
};
_executeFetch();
}
page() {
if (!this.activityservice.since[this.type][this.element.id].finished) {
this.loading = true;
this.fetchUntilResults();
} else {
this.loading = false;
}
}
loadingNewer() {
return this.activityservice.runningNewer;
}
}
let activityComponent = {
templateUrl: OC.linkTo('deck', 'templates/part.card.activity.html'),
controller: ActivityController,
bindings: {
type: '@',
element: '='
}
};
export default activityComponent;

View File

@@ -434,6 +434,14 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
};
};
$scope.colorValue = function(color) {
const re = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;
if (re.test(color)) {
return color;
}
return '';
};
$scope.attachmentCount = function(card) {
if (Array.isArray(card.attachments)) {
return card.attachments.filter((obj) => obj.deletedAt === 0).length;

View File

@@ -4,20 +4,20 @@
* @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/>.
*
*
*/
import app from '../app/App.js';
@@ -32,6 +32,10 @@ app.directive('avatar', function() {
link: function(scope, element, attr){
scope.uid = attr.displayname;
scope.displayname = attr.displayname;
scope.size = attr.size;
if (typeof scope.size === 'undefined') {
scope.size = 32;
}
var value = attr.user;
var avatardiv = $(element).find('.avatardiv');
if(typeof attr.contactsmenu !== 'undefined' && attr.contactsmenu !== 'false') {
@@ -44,8 +48,8 @@ app.directive('avatar', function() {
placement: 'top'
});
}
avatardiv.avatar(value, 32, false, false, false, attr.displayname);
avatardiv.avatar(value, scope.size, false, false, false, attr.displayname);
},
controller: function () {}
};
});
});

View File

@@ -0,0 +1,38 @@
/*
* @copyright Copyright (c) 2018 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/>.
*
*/
import app from '../app/App.js';
app.directive('bindHtmlCompile', function ($compile) {
'use strict';
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
element.html(value);
$compile(element.contents())(scope);
});
}
};
});

View File

@@ -14,9 +14,11 @@ import './app/Run.js';
import ListController from 'controller/ListController.js';
import attachmentListComponent from './controller/AttachmentController.js';
import activityComponent from './controller/ActivityController.js';
app.controller('ListController', ListController);
app.component('attachmentListComponent', attachmentListComponent);
app.component('activityComponent', activityComponent);
// require all the js files from subdirectories

46
js/package-lock.json generated
View File

@@ -2970,8 +2970,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -2992,14 +2991,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3014,20 +3011,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -3144,8 +3138,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -3157,7 +3150,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3172,7 +3164,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -3180,14 +3171,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -3206,7 +3195,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3287,8 +3275,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -3300,7 +3287,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -3386,8 +3372,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3423,7 +3408,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -3443,7 +3427,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3487,14 +3470,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@@ -4917,6 +4898,11 @@
"integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==",
"dev": true
},
"ng-infinite-scroll": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ng-infinite-scroll/-/ng-infinite-scroll-1.3.0.tgz",
"integrity": "sha1-wumNj9E0sFJaTSz1jJXZtYN1URI="
},
"ng-sortable": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ng-sortable/-/ng-sortable-1.3.8.tgz",

View File

@@ -17,6 +17,7 @@
"babel-polyfill": "^6.26.0",
"markdown-it": "^8.4.2",
"markdown-it-link-target": "^1.0.2",
"ng-infinite-scroll": "^1.3.0",
"ng-sortable": "^1.3.8",
"ui-select": "^0.19.8"
},

View File

@@ -0,0 +1,187 @@
/*
* @copyright Copyright (c) 2018 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/>.
*
*/
import app from '../app/App.js';
const DECK_ACTIVITY_TYPE_BOARD = 'deck_board';
const DECK_ACTIVITY_TYPE_CARD = 'deck_card';
/* global OC oc_requesttoken */
class ActivityService {
constructor ($rootScope, $filter, $http, $q) {
this.running = false;
this.runningNewer = false;
this.$filter = $filter;
this.$http = $http;
this.$q = $q;
this.data = {};
this.data[DECK_ACTIVITY_TYPE_BOARD] = {};
this.data[DECK_ACTIVITY_TYPE_CARD] = {};
this.since = {
deck_card: {
},
deck_board: {
},
};
}
static getUrl(type, id, since) {
if (type === DECK_ACTIVITY_TYPE_CARD) {
return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'filter?format=json&object_type=deck_card&object_id=' + id + '&limit=50&since=' + since;
}
if (type === DECK_ACTIVITY_TYPE_BOARD) {
return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'deck?format=json&limit=50&since=' + since;
}
}
fetchCardActivities(type, id, since) {
this.running = true;
this.checkData(type, id);
const self = this;
return this.$http.get(ActivityService.getUrl(type, id, since)).then(function (response) {
const objects = response.data.ocs.data;
for (let index in objects) {
if (objects.hasOwnProperty(index)) {
let item = objects[index];
self.addItem(type, id, item);
if (item.activity_id > self.since[type][id].latest) {
self.since[type][id].latest = item.activity_id;
}
}
}
self.data[type][id].sort(function(a, b) {
return b.activity_id - a.activity_id;
});
self.since[type][id].oldest = response.headers('X-Activity-Last-Given');
self.running = false;
return response;
}, function (error) {
if (error.status === 304) {
self.since[type][id].finished = true;
}
self.running = false;
});
}
fetchMoreActivities(type, id) {
this.checkData(type, id);
if (this.running === true) {
return this.runningPromise;
}
if (!this.since[type][id].finished) {
this.runningPromise = this.fetchCardActivities(type, id, this.since[type][id].oldest);
return this.runningPromise;
}
return Promise.reject();
}
checkData(type, id) {
if (!Array.isArray(this.data[type][id])) {
this.data[type][id] = [];
}
if (typeof this.since[type][id] === 'undefined') {
this.since[type][id] = {
latest: 0,
oldestCatchedUp: false,
oldest: '0',
finished: false,
};
}
}
addItem(type, id, item) {
const existingEntry = this.data[type][id].findIndex((entry) => { return entry.activity_id === item.activity_id; });
if (existingEntry !== -1) {
return;
}
/** check if the fetched item from all deck activities is actually related */
const isUnrelatedBoard = (item.object_type === DECK_ACTIVITY_TYPE_BOARD && item.object_id !== id);
const isUnrelatedCard = (item.object_type === DECK_ACTIVITY_TYPE_CARD && item.subject_rich[1].board && item.subject_rich[1].board.id !== id);
if (type === DECK_ACTIVITY_TYPE_BOARD && (isUnrelatedBoard || isUnrelatedCard)) {
return;
}
item.timestamp = new Date(item.datetime).getTime();
this.data[type][id].push(item);
}
/**
* Fetch newer activities starting from the latest ones that are in cache
*
* @param type
* @param id
*/
fetchNewerActivities(type, id) {
if (this.since[type][id].latest === 0) {
return Promise.resolve();
}
let self = this;
return this.fetchNewer(type, id).then(function() {
return self.fetchNewerActivities(type, id);
});
}
fetchNewer(type, id) {
const deferred = this.$q.defer();
this.running = true;
this.runningNewer = true;
const self = this;
this.$http.get(ActivityService.getUrl(type, id, this.since[type][id].latest) + '&sort=asc').then(function (response) {
let objects = response.data.ocs.data;
let data = [];
for (let index in objects) {
if (objects.hasOwnProperty(index)) {
let item = objects[index];
self.addItem(type, id, item);
}
}
self.data[type][id].sort(function(a, b) {
return b.activity_id - a.activity_id;
});
self.since[type][id].latest = response.headers('X-Activity-Last-Given');
self.data[type][id] = data.concat(self.data[type][id]);
self.running = false;
self.runningNewer = false;
deferred.resolve(objects);
}, function (error) {
self.runningNewer = false;
self.running = false;
});
return deferred.promise;
}
getData(type, id) {
if (!Array.isArray(this.data[type][id])) {
return [];
}
return this.data[type][id];
}
}
app.service('ActivityService', ActivityService);
export default ActivityService;
export {DECK_ACTIVITY_TYPE_BOARD, DECK_ACTIVITY_TYPE_CARD};

View File

@@ -0,0 +1,481 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use InvalidArgumentException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignedUsers;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Service\PermissionService;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\IL10N;
use OCP\IUser;
class ActivityManager {
private $manager;
private $userId;
private $permissionService;
private $boardMapper;
private $cardMapper;
private $attachmentMapper;
private $aclMapper;
private $stackMapper;
private $l10n;
const DECK_OBJECT_BOARD = 'deck_board';
const DECK_OBJECT_CARD = 'deck_card';
const SUBJECT_BOARD_CREATE = 'board_create';
const SUBJECT_BOARD_UPDATE = 'board_update';
const SUBJECT_BOARD_UPDATE_TITLE = 'board_update_title';
const SUBJECT_BOARD_UPDATE_ARCHIVED = 'board_update_archived';
const SUBJECT_BOARD_DELETE = 'board_delete';
const SUBJECT_BOARD_RESTORE = 'board_restore';
const SUBJECT_BOARD_SHARE = 'board_share';
const SUBJECT_BOARD_UNSHARE = 'board_unshare';
const SUBJECT_STACK_CREATE = 'stack_create';
const SUBJECT_STACK_UPDATE = 'stack_update';
const SUBJECT_STACK_UPDATE_TITLE = 'stack_update_title';
const SUBJECT_STACK_UPDATE_ORDER = 'stack_update_order';
const SUBJECT_STACK_DELETE = 'stack_delete';
const SUBJECT_CARD_CREATE = 'card_create';
const SUBJECT_CARD_DELETE = 'card_delete';
const SUBJECT_CARD_RESTORE = 'card_restore';
const SUBJECT_CARD_UPDATE = 'card_update';
const SUBJECT_CARD_UPDATE_TITLE = 'card_update_title';
const SUBJECT_CARD_UPDATE_DESCRIPTION = 'card_update_description';
const SUBJECT_CARD_UPDATE_DUEDATE = 'card_update_duedate';
const SUBJECT_CARD_UPDATE_ARCHIVE = 'card_update_archive';
const SUBJECT_CARD_UPDATE_UNARCHIVE = 'card_update_unarchive';
const SUBJECT_CARD_UPDATE_STACKID = 'card_update_stackId';
const SUBJECT_CARD_USER_ASSIGN = 'card_user_assign';
const SUBJECT_CARD_USER_UNASSIGN = 'card_user_unassign';
const SUBJECT_ATTACHMENT_CREATE = 'attachment_create';
const SUBJECT_ATTACHMENT_UPDATE = 'attachment_update';
const SUBJECT_ATTACHMENT_DELETE = 'attachment_delete';
const SUBJECT_ATTACHMENT_RESTORE = 'attachment_restore';
const SUBJECT_LABEL_CREATE = 'label_create';
const SUBJECT_LABEL_UPDATE = 'label_update';
const SUBJECT_LABEL_DELETE = 'label_delete';
const SUBJECT_LABEL_ASSIGN = 'label_assign';
const SUBJECT_LABEL_UNASSING = 'label_unassign';
public function __construct(
IManager $manager,
PermissionService $permissionsService,
BoardMapper $boardMapper,
CardMapper $cardMapper,
StackMapper $stackMapper,
AttachmentMapper $attachmentMapper,
AclMapper $aclMapper,
IL10N $l10n,
$userId
) {
$this->manager = $manager;
$this->permissionService = $permissionsService;
$this->boardMapper = $boardMapper;
$this->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper;
$this->attachmentMapper = $attachmentMapper;
$this->aclMapper = $aclMapper;
$this->l10n = $l10n;
$this->userId = $userId;
}
/**
* @param $subjectIdentifier
* @param array $subjectParams
* @param bool $ownActivity
* @return string
*/
public function getActivityFormat($subjectIdentifier, $subjectParams = [], $ownActivity = false) {
$subject = '';
switch ($subjectIdentifier) {
case self::SUBJECT_BOARD_CREATE:
$subject = $ownActivity ? $this->l10n->t('You have created a new board {board}'): $this->l10n->t('{user} has created a new board {board}');
break;
case self::SUBJECT_BOARD_DELETE:
$subject = $ownActivity ? $this->l10n->t('You have deleted the board {board}') : $this->l10n->t('{user} has deleted the board {board}');
break;
case self::SUBJECT_BOARD_RESTORE:
$subject = $ownActivity ? $this->l10n->t('You have restored the board {board}') : $this->l10n->t('{user} has restored the board {board}');
break;
case self::SUBJECT_BOARD_SHARE:
$subject = $ownActivity ? $this->l10n->t('You have shared the board {board} with {acl}') : $this->l10n->t('{user} has shared the board {board} with {sharee}');
break;
case self::SUBJECT_BOARD_UNSHARE:
$subject = $ownActivity ? $this->l10n->t('You have removed {acl} from the board {board}') : $this->l10n->t('{user} has removed {acl} from the board {board}');
break;
case self::SUBJECT_BOARD_UPDATE_TITLE:
$subject = $ownActivity ? $this->l10n->t('You have renamed the board {before} to {board}') : $this->l10n->t('{user} has has renamed the board {before} to {board}');
break;
case self::SUBJECT_BOARD_UPDATE_ARCHIVED:
if (isset($subjectParams['after']) && $subjectParams['after']) {
$subject = $ownActivity ? $this->l10n->t('You have archived the board {board}') : $this->l10n->t('{user} has archived the board {before}');
} else {
$subject = $ownActivity ? $this->l10n->t('You have unarchived the board {board}') : $this->l10n->t('{user} has unarchived the board {before}');
}
break;
case self::SUBJECT_STACK_CREATE:
$subject = $ownActivity ? $this->l10n->t('You have created a new stack {stack} on {board}') : $this->l10n->t('{user} has created a new stack {stack} on {board}');
break;
case self::SUBJECT_STACK_UPDATE:
$subject = $ownActivity ? $this->l10n->t('You have created a new stack {stack} on {board}') : $this->l10n->t('{user} has created a new stack {stack} on {board}');
break;
case self::SUBJECT_STACK_UPDATE_TITLE:
$subject = $ownActivity ? $this->l10n->t('You have renamed a new stack {before} to {stack} on {board}') : $this->l10n->t('{user} has renamed a new stack {before} to {stack} on {board}');
break;
case self::SUBJECT_STACK_DELETE:
$subject = $ownActivity ? $this->l10n->t('You have deleted {stack} on {board}') : $this->l10n->t('{user} has deleted {stack} on {board}');
break;
case self::SUBJECT_CARD_CREATE:
$subject = $ownActivity ? $this->l10n->t('You have created {card} in {stack} on {board}') : $this->l10n->t('{user} has created {card} in {stack} on {board}');
break;
case self::SUBJECT_CARD_DELETE:
$subject = $ownActivity ? $this->l10n->t('You have deleted {card} in {stack} on {board}') : $this->l10n->t('{user} has deleted {card} in {stack} on {board}');
break;
case self::SUBJECT_CARD_UPDATE_TITLE:
$subject = $ownActivity ? $this->l10n->t('You have renamed the card {before} to {card}') : $this->l10n->t('{user} has renamed the card {before} to {card}');
break;
case self::SUBJECT_CARD_UPDATE_DESCRIPTION:
if (!isset($subjectParams['before'])) {
$subject = $ownActivity ? $this->l10n->t('You have added a description to {card} in {stack} on {board}') : $this->l10n->t('{user} has added a description to {card} in {stack} on {board}');
} else {
$subject = $ownActivity ? $this->l10n->t('You have updated the description of {card} in {stack} on {board}') : $this->l10n->t('{user} has updated the description {card} in {stack} on {board}');
}
break;
case self::SUBJECT_CARD_UPDATE_ARCHIVE:
$subject = $ownActivity ? $this->l10n->t('You have archived {card} in {stack} on {board}') : $this->l10n->t('{user} has archived {card} in {stack} on {board}');
break;
case self::SUBJECT_CARD_UPDATE_UNARCHIVE:
$subject = $ownActivity ? $this->l10n->t('You have unarchived {card} in {stack} on {board}') : $this->l10n->t('{user} has unarchived {card} in {stack} on {board}');
break;
case self::SUBJECT_CARD_UPDATE_DUEDATE:
if (!isset($subjectParams['after'])) {
$subject = $ownActivity ? $this->l10n->t('You have removed the due date of {card}') : $this->l10n->t('{user} has removed the due date of {card}');
} else if (isset($subjectParams['before']) && !isset($subjectParams['after'])) {
$subject = $ownActivity ? $this->l10n->t('You have set the due date of {card} to {after}') : $this->l10n->t('{user} has set the due date of {card} to {after}');
} else {
$subject = $ownActivity ? $this->l10n->t('You have updated the due date of {card} to {after}') : $this->l10n->t('{user} has updated the due date of {card} to {after}');
}
break;
case self::SUBJECT_LABEL_ASSIGN:
$subject = $ownActivity ? $this->l10n->t('You have added the label {label} to {card} in {stack} on {board}') : $this->l10n->t('{user} has added the label {label} to {card} in {stack} on {board}');
break;
case self::SUBJECT_LABEL_UNASSING:
$subject = $ownActivity ? $this->l10n->t('You have removed the label {label} from {card} in {stack} on {board}') : $this->l10n->t('{user} has removed the label {label} from {card} in {stack} on {board}');
break;
case self::SUBJECT_CARD_USER_ASSIGN:
$subject = $ownActivity ? $this->l10n->t('You have assigned {assigneduser} to {card} on {board}') : $this->l10n->t('{user} has assigned {assigneduser} to {card} on {board}');
break;
case self::SUBJECT_CARD_USER_UNASSIGN:
$subject = $ownActivity ? $this->l10n->t('You have unassigned {assigneduser} from {card} on {board}') : $this->l10n->t('{user} has unassigned {assigneduser} from {card} on {board}');
break;
case self::SUBJECT_CARD_UPDATE_STACKID:
$subject = $ownActivity ? $this->l10n->t('You have moved the card {card} from {stackBefore} to {stack}') : $this->l10n->t('{user} has moved the card {card} from {stackBefore} to {stack}');
break;
case self::SUBJECT_ATTACHMENT_CREATE:
$subject = $ownActivity ? $this->l10n->t('You have added the attachment {attachment} to {card}') : $this->l10n->t('{user} has added the attachment {attachment} to {card}');
break;
case self::SUBJECT_ATTACHMENT_UPDATE:
$subject = $ownActivity ? $this->l10n->t('You have updated the attachment {attachment} on {card}') : $this->l10n->t('{user} has updated the attachment {attachment} to {card}');
break;
case self::SUBJECT_ATTACHMENT_DELETE:
$subject = $ownActivity ? $this->l10n->t('You have deleted the attachment {attachment} from {card}') : $this->l10n->t('{user} has deleted the attachment {attachment} to {card}');
break;
case self::SUBJECT_ATTACHMENT_RESTORE:
$subject = $ownActivity ? $this->l10n->t('You have restored the attachment {attachment} to {card}') : $this->l10n->t('{user} has restored the attachment {attachment} to {card}');
break;
default:
break;
}
return $subject;
}
public function triggerEvent($objectType, $entity, $subject, $additionalParams = []) {
try {
$event = $this->createEvent($objectType, $entity, $subject, $additionalParams);
$this->sendToUsers($event);
} catch (\Exception $e) {
// Ignore exception for undefined activities on update events
}
}
/**
*
* @param $objectType
* @param ChangeSet $changeSet
* @param $subject
* @throws \Exception
*/
public function triggerUpdateEvents($objectType, ChangeSet $changeSet, $subject) {
$previousEntity = $changeSet->getBefore();
$entity = $changeSet->getAfter();
$events = [];
if ($previousEntity !== null) {
foreach ($entity->getUpdatedFields() as $field => $value) {
$getter = 'get' . ucfirst($field);
$subject = $subject . '_' . $field;
$changes = [
'before' => $previousEntity->$getter(),
'after' => $entity->$getter()
];
if ($changes['before'] !== $changes['after']) {
try {
$event = $this->createEvent($objectType, $entity, $subject, $changes);
$events[] = $event;
} catch (\Exception $e) {
// Ignore exception for undefined activities on update events
}
}
}
} else {
try {
$events = [$this->createEvent($objectType, $entity, $subject)];
} catch (\Exception $e) {
// Ignore exception for undefined activities on update events
}
}
foreach ($events as $event) {
$this->sendToUsers($event);
}
}
/**
* @param $objectType
* @param $entity
* @param $subject
* @param array $additionalParams
* @return IEvent
* @throws \Exception
*/
private function createEvent($objectType, $entity, $subject, $additionalParams = []) {
try {
$object = $this->findObjectForEntity($objectType, $entity);
} catch (DoesNotExistException $e) {
} catch (MultipleObjectsReturnedException $e) {
\OC::$server->getLogger()->error('Could not create activity entry for ' . $subject . '. Entity not found.', $entity);
return null;
}
/**
* Automatically fetch related details for subject parameters
* depending on the subject
*/
$subjectParams = [];
$message = null;
switch ($subject) {
// No need to enhance parameters since entity already contains the required data
case self::SUBJECT_BOARD_CREATE:
case self::SUBJECT_BOARD_UPDATE_TITLE:
case self::SUBJECT_BOARD_UPDATE_ARCHIVED:
case self::SUBJECT_BOARD_DELETE:
case self::SUBJECT_BOARD_RESTORE:
// Not defined as there is no activity for
// case self::SUBJECT_BOARD_UPDATE_COLOR
break;
case self::SUBJECT_STACK_CREATE:
case self::SUBJECT_STACK_UPDATE:
case self::SUBJECT_STACK_UPDATE_TITLE:
case self::SUBJECT_STACK_UPDATE_ORDER:
case self::SUBJECT_STACK_DELETE:
$subjectParams = $this->findDetailsForStack($entity->getId());
break;
case self::SUBJECT_CARD_CREATE:
case self::SUBJECT_CARD_DELETE:
case self::SUBJECT_CARD_UPDATE_ARCHIVE:
case self::SUBJECT_CARD_UPDATE_UNARCHIVE:
case self::SUBJECT_CARD_UPDATE_TITLE:
case self::SUBJECT_CARD_UPDATE_DESCRIPTION:
case self::SUBJECT_CARD_UPDATE_DUEDATE:
case self::SUBJECT_CARD_UPDATE_STACKID:
case self::SUBJECT_LABEL_ASSIGN:
case self::SUBJECT_LABEL_UNASSING:
case self::SUBJECT_CARD_USER_ASSIGN:
case self::SUBJECT_CARD_USER_UNASSIGN:
$subjectParams = $this->findDetailsForCard($entity->getId());
$object = $entity;
break;
case self::SUBJECT_ATTACHMENT_CREATE:
case self::SUBJECT_ATTACHMENT_UPDATE:
case self::SUBJECT_ATTACHMENT_DELETE:
case self::SUBJECT_ATTACHMENT_RESTORE:
$subjectParams = $this->findDetailsForAttachment($entity->getId());
$object = $subjectParams['card'];
break;
case self::SUBJECT_BOARD_SHARE:
case self::SUBJECT_BOARD_UNSHARE:
$subjectParams = $this->findDetailsForAcl($entity->getId());
break;
default:
throw new \Exception('Unknown subject for activity.');
break;
}
if ($subject === self::SUBJECT_CARD_UPDATE_DESCRIPTION){
$subjectParams['diff'] = true;
}
if ($subject === self::SUBJECT_CARD_UPDATE_STACKID) {
$subjectParams['stackBefore'] = $this->stackMapper->find($additionalParams['before']);
}
$event = $this->manager->generateEvent();
$event->setApp('deck')
->setType('deck')
->setAuthor($this->userId)
->setObject($objectType, (int)$object->getId(), $object->getTitle())
->setSubject($subject, array_merge($subjectParams, $additionalParams))
->setTimestamp(time());
if ($message !== null) {
$event->setMessage($message);
}
return $event;
}
/**
* Publish activity to all users that are part of the board of a given object
*
* @param IEvent $event
*/
private function sendToUsers(IEvent $event) {
switch ($event->getObjectType()) {
case self::DECK_OBJECT_BOARD:
$mapper = $this->boardMapper;
break;
case self::DECK_OBJECT_CARD:
$mapper = $this->cardMapper;
break;
}
$boardId = $mapper->findBoardId($event->getObjectId());
/** @var IUser $user */
foreach ($this->permissionService->findUsers($boardId) as $user) {
$event->setAffectedUser($user->getUID());
/** @noinspection DisconnectedForeachInstructionInspection */
$this->manager->publish($event);
}
}
/**
* @param $objectType
* @param $entity
* @return null|\OCA\Deck\Db\RelationalEntity|\OCP\AppFramework\Db\Entity
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
private function findObjectForEntity($objectType, $entity) {
$className = \get_class($entity);
$objectId = null;
if ($objectType === self::DECK_OBJECT_CARD) {
switch ($className) {
case Card::class:
$objectId = $entity->getId();
break;
case Attachment::class:
case Label::class:
case AssignedUsers::class:
$objectId = $entity->getCardId();
break;
default:
throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType);
}
return $this->cardMapper->find($objectId);
}
if ($objectType === self::DECK_OBJECT_BOARD) {
switch ($className) {
case Board::class:
$objectId = $entity->getId();
break;
case Label::class:
case Stack::class:
case Acl::class:
$objectId = $entity->getBoardId();
break;
default:
throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType);
}
return $this->boardMapper->find($objectId);
}
throw new InvalidArgumentException('No entity relation present for '. $className . ' to ' . $objectType);
}
private function findDetailsForStack($stackId) {
$stack = $this->stackMapper->find($stackId);
$board = $this->boardMapper->find($stack->getBoardId());
return [
'stack' => $stack,
'board' => $board
];
}
private function findDetailsForCard($cardId) {
$card = $this->cardMapper->find($cardId);
$stack = $this->stackMapper->find($card->getStackId());
$board = $this->boardMapper->find($stack->getBoardId());
return [
'card' => $card,
'stack' => $stack,
'board' => $board
];
}
private function findDetailsForAttachment($attachmentId) {
$attachment = $this->attachmentMapper->find($attachmentId);
$data = $this->findDetailsForCard($attachment->getCardId());
return array_merge($data, ['attachment' => $attachment]);
}
private function findDetailsForAcl($aclId) {
$acl = $this->aclMapper->find($aclId);
$board = $this->boardMapper->find($acl->getBoardId());
return [
'acl' => $acl,
'board' => $board
];
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
class ChangeSet implements \JsonSerializable {
private $before;
private $after;
private $diff = false;
public function __construct($before = null, $after = null) {
if ($before !== null) {
$this->setBefore($before);
}
if ($after !== null) {
$this->setAfter($after);
}
}
public function enableDiff() {
$this->diff = true;
}
public function getDiff() {
return $this->diff;
}
public function setBefore($before) {
if (is_object($before)) {
$this->before = clone $before;
} else {
$this->before = $before;
}
}
public function setAfter($after) {
if (is_object($after)) {
$this->after = clone $after;
} else {
$this->after = $after;
}
}
public function getBefore() {
return $this->before;
}
public function getAfter() {
return $this->after;
}
/**
* Specify data which should be serialized to JSON
*
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize() {
return [
'before' => $this->getBefore(),
'after' => $this->getAfter(),
'diff' => $this->getDiff(),
'type' => get_class($this->before)
];
}
}

View File

@@ -0,0 +1,265 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use cogpowered\FineDiff\Diff;
use OCA\Deck\Db\Acl;
use OCP\Activity\IEvent;
use OCP\Activity\IProvider;
use OCP\IURLGenerator;
use OCP\IUserManager;
class DeckProvider implements IProvider {
/** @var string */
private $userId;
/** @var IURLGenerator */
private $urlGenerator;
/** @var ActivityManager */
private $activityManager;
/** @var IUserManager */
private $userManager;
public function __construct(IURLGenerator $urlGenerator, ActivityManager $activityManager, IUserManager $userManager, $userId) {
$this->userId = $userId;
$this->urlGenerator = $urlGenerator;
$this->activityManager = $activityManager;
$this->userManager = $userManager;
}
/**
* @param string $language The language which should be used for translating, e.g. "en"
* @param IEvent $event The current event which should be parsed
* @param IEvent|null $previousEvent A potential previous event which you can combine with the current one.
* To do so, simply use setChildEvent($previousEvent) after setting the
* combined subject on the current event.
* @return IEvent
* @throws \InvalidArgumentException Should be thrown if your provider does not know this event
* @since 11.0.0
*/
public function parse($language, IEvent $event, IEvent $previousEvent = null) {
if ($event->getApp() !== 'deck') {
throw new \InvalidArgumentException();
}
$event = $this->getIcon($event);
$subjectIdentifier = $event->getSubject();
$subjectParams = $event->getSubjectParameters();
$ownActivity = ($event->getAuthor() === $this->userId);
/**
* Map stored parameter objects to rich string types
*/
$board = null;
if ($event->getObjectType() === ActivityManager::DECK_OBJECT_BOARD) {
$board = [
'type' => 'highlight',
'id' => $event->getObjectId(),
'name' => $event->getObjectName(),
'link' => $this->deckUrl('/board/' . $event->getObjectId()),
];
}
$card = null;
if ($event->getObjectType() === ActivityManager::DECK_OBJECT_CARD) {
$card = [
'type' => 'highlight',
'id' => $event->getObjectId(),
'name' => $event->getObjectName(),
];
if (array_key_exists('board', $subjectParams)) {
$archivedParam = $subjectParams['card']['archived'] ? 'archived' : '';
$card['link'] = $this->deckUrl('/board/' . $subjectParams['board']['id'] . '/' . $archivedParam . '/card/' . $event->getObjectId());
}
}
$author = $event->getAuthor();
$user = $this->userManager->get($author);
$params = [
'board' => $board,
'card' => $card,
'user' => [
'type' => 'user',
'id' => $author,
'name' => $user !== null ? $user->getDisplayName() : $author
]
];
$params = $this->parseParamForBoard('board', $subjectParams, $params);
$params = $this->parseParamForStack('stack', $subjectParams, $params);
$params = $this->parseParamForStack('stackBefore', $subjectParams, $params);
$params = $this->parseParamForAttachment('attachment', $subjectParams, $params);
$params = $this->parseParamForLabel($subjectParams, $params);
$params = $this->parseParamForAssignedUser($subjectParams, $params);
$params = $this->parseParamForAcl($subjectParams, $params);
$params = $this->parseParamForChanges($subjectParams, $params, $event);
try {
$subject = $this->activityManager->getActivityFormat($subjectIdentifier, $subjectParams, $ownActivity);
$event->setParsedSubject($subject);
$event->setRichSubject($subject, $params);
} catch (\Exception $e) {
}
return $event;
}
private function getIcon(IEvent $event) {
$event->setIcon($this->urlGenerator->imagePath('deck', 'deck-dark.svg'));
if (strpos($event->getSubject(), '_update') !== false) {
$event->setIcon($this->urlGenerator->imagePath('files', 'change.svg'));
}
if (strpos($event->getSubject(), '_create') !== false) {
$event->setIcon($this->urlGenerator->imagePath('files', 'add-color.svg'));
}
if (strpos($event->getSubject(), '_delete') !== false) {
$event->setIcon($this->urlGenerator->imagePath('files', 'delete-color.svg'));
}
if (strpos($event->getSubject(), 'archive') !== false) {
$event->setIcon($this->urlGenerator->imagePath('deck', 'archive.svg'));
}
if (strpos($event->getSubject(), '_restore') !== false) {
$event->setIcon($this->urlGenerator->imagePath('core', 'actions/history.svg'));
}
if (strpos($event->getSubject(), 'attachment_') !== false) {
$event->setIcon($this->urlGenerator->imagePath('core', 'places/files.svg'));
}
return $event;
}
private function parseParamForBoard($paramName, $subjectParams, $params) {
if (array_key_exists($paramName, $subjectParams)) {
$params[$paramName] = [
'type' => 'highlight',
'id' => $subjectParams[$paramName]['id'],
'name' => $subjectParams[$paramName]['title'],
'link' => $this->deckUrl('/board/' . $subjectParams[$paramName]['id'] . '/'),
];
}
return $params;
}
private function parseParamForStack($paramName, $subjectParams, $params) {
if (array_key_exists($paramName, $subjectParams)) {
$params[$paramName] = [
'type' => 'highlight',
'id' => $subjectParams[$paramName]['id'],
'name' => $subjectParams[$paramName]['title'],
];
}
return $params;
}
private function parseParamForAttachment($paramName, $subjectParams, $params) {
if (array_key_exists($paramName, $subjectParams)) {
$params[$paramName] = [
'type' => 'highlight',
'id' => $subjectParams[$paramName]['id'],
'name' => $subjectParams[$paramName]['data'],
'link' => $this->urlGenerator->linkToRoute('deck.attachment.display', ['cardId' => $subjectParams['card']['id'], 'attachmentId' => $subjectParams['attachment']['id']]),
];
}
return $params;
}
private function parseParamForAssignedUser($subjectParams, $params) {
if (array_key_exists('assigneduser', $subjectParams)) {
$user = $this->userManager->get($subjectParams['assigneduser']);
$params['assigneduser'] = [
'type' => 'user',
'id' => $subjectParams['assigneduser'],
'name' => $user !== null ? $user->getDisplayName() : $subjectParams['assigneduser']
];
}
return $params;
}
private function parseParamForLabel($subjectParams, $params) {
if (array_key_exists('label', $subjectParams)) {
$params['label'] = [
'type' => 'highlight',
'id' => $subjectParams['label']['id'],
'name' => $subjectParams['label']['title']
];
}
return $params;
}
private function parseParamForAcl($subjectParams, $params) {
if (array_key_exists('acl', $subjectParams)) {
if ($subjectParams['acl']['type'] === Acl::PERMISSION_TYPE_USER) {
$user = $this->userManager->get($subjectParams['acl']['participant']);
$params['acl'] = [
'type' => 'user',
'id' => $subjectParams['acl']['participant'],
'name' => $user !== null ? $user->getDisplayName() : $subjectParams['acl']['participant']
];
} else {
$params['acl'] = [
'type' => 'highlight',
'id' => $subjectParams['acl']['participant'],
'name' => $subjectParams['acl']['participant']
];
}
}
return $params;
}
/**
* Add diff to message if the subject parameter 'diff' is set, otherwise
* the changed values are added to before/after
*
* @param $subjectParams
* @param $params
* @return mixed
*/
private function parseParamForChanges($subjectParams, $params, $event) {
if (array_key_exists('diff', $subjectParams) && $subjectParams['diff']) {
$diff = new Diff();
$event->setMessage($subjectParams['after']);
$event->setParsedMessage('<pre class="visualdiff">' . $diff->render($subjectParams['before'], $subjectParams['after']) . '</pre>');
return $params;
}
if (array_key_exists('before', $subjectParams)) {
$params['before'] = [
'type' => 'highlight',
'id' => $subjectParams['before'],
'name' => $subjectParams['before']
];
}
if (array_key_exists('after', $subjectParams)) {
$params['after'] = [
'type' => 'highlight',
'id' => $subjectParams['after'],
'name' => $subjectParams['after']
];
}
return $params;
}
public function deckUrl($endpoint) {
return $this->urlGenerator->linkToRoute('deck.page.index') . '#!' . $endpoint;
}
}

92
lib/Activity/Filter.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use OCP\IL10N;
use OCP\IURLGenerator;
class Filter implements \OCP\Activity\IFilter {
private $l10n;
private $urlGenerator;
public function __construct(
IL10N $l10n,
IURLGenerator $urlGenerator
) {
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
}
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'deck';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l10n->t('Deck');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 90;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 11.0.0
*/
public function getIcon() {
return $this->urlGenerator->imagePath('deck', 'deck-dark.svg');
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function filterTypes(array $types) {
return $types;
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function allowedApps() {
return ['deck'];
}
}

86
lib/Activity/Setting.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
class Setting implements \OCP\Activity\ISetting {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'deck';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return 'Deck';
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 90;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function canChangeStream() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledStream() {
return true;
}
/**
* @return bool True when the option can be changed for the mail
* @since 11.0.0
*/
public function canChangeMail() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledMail() {
return false;
}
}

View File

@@ -24,7 +24,9 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
@@ -52,6 +54,8 @@ class AttachmentService {
private $cache;
/** @var IL10N */
private $l10n;
/** @var ActivityManager */
private $activityManager;
/**
* AttachmentService constructor.
@@ -65,7 +69,7 @@ class AttachmentService {
* @param IL10N $l10n
* @throws \OCP\AppFramework\QueryException
*/
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n) {
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n, ActivityManager $activityManager) {
$this->attachmentMapper = $attachmentMapper;
$this->cardMapper = $cardMapper;
$this->permissionService = $permissionService;
@@ -73,6 +77,7 @@ class AttachmentService {
$this->application = $application;
$this->cache = $cacheFactory->createDistributed('deck-card-attachments-');
$this->l10n = $l10n;
$this->activityManager = $activityManager;
// Register shipped attachment services
// TODO: move this to a plugin based approach once we have different types of attachments
@@ -168,7 +173,7 @@ class AttachmentService {
}
if ($data === false || $data === null) {
throw new BadRequestException('data must be provided');
//throw new BadRequestException('data must be provided');
}
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
@@ -200,6 +205,7 @@ class AttachmentService {
} catch (InvalidAttachmentType $e) {
// just store the data
}
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_CREATE);
return $attachment;
}
@@ -261,7 +267,7 @@ class AttachmentService {
}
if ($data === false || $data === null) {
throw new BadRequestException('data must be provided');
//throw new BadRequestException('data must be provided');
}
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
@@ -284,6 +290,7 @@ class AttachmentService {
} catch (InvalidAttachmentType $e) {
// just store the data
}
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_UPDATE);
return $attachment;
}
@@ -317,13 +324,16 @@ class AttachmentService {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment);
} catch (InvalidAttachmentType $e) {
// just delete without further action
}
return $this->attachmentMapper->delete($attachment);
$attachment = $this->attachmentMapper->delete($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment;
}
public function restore($cardId, $attachmentId) {
@@ -344,10 +354,12 @@ class AttachmentService {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$attachment->setDeletedAt(0);
return $this->attachmentMapper->update($attachment);
$attachment = $this->attachmentMapper->update($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_RESTORE);
return $attachment;
}
} catch (InvalidAttachmentType $e) {
}
throw new NoPermissionException('Restore is not allowed.');
}
}
}

View File

@@ -23,6 +23,8 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignedUsersMapper;
@@ -51,6 +53,7 @@ class BoardService {
private $userManager;
private $groupManager;
private $userId;
private $activityManager;
public function __construct(
BoardMapper $boardMapper,
@@ -62,6 +65,7 @@ class BoardService {
AssignedUsersMapper $assignedUsersMapper,
IUserManager $userManager,
IGroupManager $groupManager,
ActivityManager $activityManager,
$userId
) {
$this->boardMapper = $boardMapper;
@@ -73,6 +77,7 @@ class BoardService {
$this->assignedUsersMapper = $assignedUsersMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->activityManager = $activityManager;
$this->userId = $userId;
}
@@ -244,7 +249,7 @@ class BoardService {
$board->setTitle($title);
$board->setOwner($userId);
$board->setColor($color);
$new_board = $this->boardMapper->insert($board);
$new_board = $this->boardMapper->insert($board);
// create new labels
$default_labels = [
@@ -270,6 +275,7 @@ class BoardService {
'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE],
'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE]
]);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $new_board, ActivityManager::SUBJECT_BOARD_CREATE);
return $new_board;
}
@@ -291,7 +297,8 @@ class BoardService {
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ);
$board = $this->find($id);
$board->setDeletedAt(time());
$this->boardMapper->update($board);
$board = $this->boardMapper->update($board);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $board, ActivityManager::SUBJECT_BOARD_DELETE);
return $board;
}
@@ -311,7 +318,9 @@ class BoardService {
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_READ);
$board = $this->find($id);
$board->setDeletedAt(0);
return $this->boardMapper->update($board);
$board = $this->boardMapper->update($board);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $board, ActivityManager::SUBJECT_BOARD_RESTORE);
return $board;
}
/**
@@ -322,7 +331,7 @@ class BoardService {
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function deleteForce($id) {
public function deleteForce($id) {
if (is_numeric($id) === false) {
throw new BadRequestException('id must be a number');
}
@@ -363,11 +372,15 @@ class BoardService {
$this->permissionService->checkPermission($this->boardMapper, $id, Acl::PERMISSION_MANAGE);
$board = $this->find($id);
$changes = new ChangeSet($board);
$board->setTitle($title);
$board->setColor($color);
$board->setArchived($archived);
$changes->setAfter($board);
$this->boardMapper->update($board); // operate on clone so we can check for updated fields
$this->boardMapper->mapOwner($board);
return $this->boardMapper->update($board);
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_BOARD_UPDATE);
return $board;
}
@@ -421,6 +434,7 @@ class BoardService {
$this->notificationHelper->sendBoardShared($boardId, $acl);
$newAcl = $this->aclMapper->insert($acl);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE);
$this->boardMapper->mapAcl($newAcl);
return $newAcl;
}
@@ -488,6 +502,7 @@ class BoardService {
$this->assignedUsersMapper->delete($assignement);
}
}
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $acl, ActivityManager::SUBJECT_BOARD_UNSHARE);
return $this->aclMapper->delete($acl);
}

View File

@@ -23,6 +23,8 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\AssignedUsers;
use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Card;
@@ -48,17 +50,19 @@ class CardService {
private $assignedUsersMapper;
private $attachmentService;
private $currentUser;
private $activityManager;
public function __construct(
CardMapper $cardMapper,
StackMapper $stackMapper,
BoardMapper $boardMapper,
LabelMapper $labelMapper,
PermissionService $permissionService,
PermissionService $permissionService,
BoardService $boardService,
NotificationHelper $notificationHelper,
AssignedUsersMapper $assignedUsersMapper,
AssignedUsersMapper $assignedUsersMapper,
AttachmentService $attachmentService,
ActivityManager $activityManager,
$userId
) {
$this->cardMapper = $cardMapper;
@@ -70,6 +74,7 @@ class CardService {
$this->notificationHelper = $notificationHelper;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
$this->activityManager = $activityManager;
$this->currentUser = $userId;
}
@@ -157,7 +162,9 @@ class CardService {
$card->setType($type);
$card->setOrder($order);
$card->setOwner($owner);
return $this->cardMapper->insert($card);
$card = $this->cardMapper->insert($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE);
return $card;
}
/**
@@ -182,6 +189,7 @@ class CardService {
$card = $this->cardMapper->find($id);
$card->setDeletedAt(time());
$this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_DELETE);
return $card;
}
@@ -204,7 +212,7 @@ class CardService {
public function update($id, $title, $stackId, $type, $order = 0, $description = '', $owner, $duedate = null, $deletedAt) {
if (is_numeric($id) === false) {
throw new BadRequestException('card id must be a number');
throw new BadRequestException('card id must be a number');
}
if ($title === false || $title === null) {
@@ -231,6 +239,7 @@ class CardService {
if ($card->getArchived()) {
throw new StatusException('Operation not allowed. This card is archived.');
}
$changes = new ChangeSet($card);
$card->setTitle($title);
$card->setStackId($stackId);
$card->setType($type);
@@ -239,7 +248,10 @@ class CardService {
$card->setDescription($description);
$card->setDuedate($duedate);
$card->setDeletedAt($deletedAt);
return $this->cardMapper->update($card);
$changes->setAfter($card);
$card = $this->cardMapper->update($card);
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_CARD, $changes, ActivityManager::SUBJECT_CARD_UPDATE);
return $card;
}
/**
@@ -350,7 +362,9 @@ class CardService {
}
$card = $this->cardMapper->find($id);
$card->setArchived(true);
return $this->cardMapper->update($card);
$newCard = $this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_ARCHIVE);
return $newCard;
}
/**
@@ -374,7 +388,9 @@ class CardService {
}
$card = $this->cardMapper->find($id);
$card->setArchived(false);
return $this->cardMapper->update($card);
$newCard = $this->cardMapper->update($card);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $newCard, ActivityManager::SUBJECT_CARD_UPDATE_UNARCHIVE);
return $newCard;
}
/**
@@ -404,7 +420,9 @@ class CardService {
if ($card->getArchived()) {
throw new StatusException('Operation not allowed. This card is archived.');
}
$label = $this->labelMapper->find($labelId);
$this->cardMapper->assignLabel($cardId, $labelId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_ASSIGN, ['label' => $label]);
}
/**
@@ -434,7 +452,9 @@ class CardService {
if ($card->getArchived()) {
throw new StatusException('Operation not allowed. This card is archived.');
}
$label = $this->labelMapper->find($labelId);
$this->cardMapper->removeLabel($cardId, $labelId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_LABEL_UNASSING, ['label' => $label]);
}
/**
@@ -462,17 +482,19 @@ class CardService {
return false;
}
}
$card = $this->cardMapper->find($cardId);
if ($userId !== $this->currentUser) {
/* Notifyuser about the card assignment */
$card = $this->cardMapper->find($cardId);
$this->notificationHelper->sendCardAssigned($card, $userId);
}
$assignment = new AssignedUsers();
$assignment->setCardId($cardId);
$assignment->setParticipant($userId);
return $this->assignedUsersMapper->insert($assignment);
$assignment = $this->assignedUsersMapper->insert($assignment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_ASSIGN, ['assigneduser' => $userId]);
return $assignment;
}
/**
@@ -498,7 +520,10 @@ class CardService {
$assignments = $this->assignedUsersMapper->find($cardId);
foreach ($assignments as $assignment) {
if ($assignment->getParticipant() === $userId) {
return $this->assignedUsersMapper->delete($assignment);
$assignment = $this->assignedUsersMapper->delete($assignment);
$card = $this->cardMapper->find($cardId);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_USER_UNASSIGN, ['assigneduser' => $userId]);
return $assignment;
}
}
throw new NotFoundException('No assignment for ' . $userId . 'found.');

View File

@@ -23,6 +23,8 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\BoardMapper;
@@ -45,6 +47,7 @@ class StackService {
private $cardService;
private $assignedUsersMapper;
private $attachmentService;
private $activityManager;
public function __construct(
StackMapper $stackMapper,
@@ -55,7 +58,8 @@ class StackService {
BoardService $boardService,
CardService $cardService,
AssignedUsersMapper $assignedUsersMapper,
AttachmentService $attachmentService
AttachmentService $attachmentService,
ActivityManager $activityManager
) {
$this->stackMapper = $stackMapper;
$this->boardMapper = $boardMapper;
@@ -66,10 +70,11 @@ class StackService {
$this->cardService = $cardService;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
$this->activityManager = $activityManager;
}
private function enrichStackWithCards($stack) {
$cards = $this->cardMapper->findAll($stack->id);
$cards = $this->cardMapper->findAll($stack->getId());
if(is_null($cards)) {
return;
@@ -195,8 +200,9 @@ class StackService {
$stack->setTitle($title);
$stack->setBoardId($boardId);
$stack->setOrder($order);
return $this->stackMapper->insert($stack);
$stack = $this->stackMapper->insert($stack);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $stack, ActivityManager::SUBJECT_STACK_CREATE);
return $stack;
}
/**
@@ -217,10 +223,11 @@ class StackService {
$stack = $this->stackMapper->find($id);
$stack->setDeletedAt(time());
$this->stackMapper->update($stack);
$stack = $this->stackMapper->update($stack);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_BOARD, $stack, ActivityManager::SUBJECT_STACK_DELETE);
$this->enrichStackWithCards($stack);
return $stack;
}
@@ -260,11 +267,15 @@ class StackService {
throw new StatusException('Operation not allowed. This board is archived.');
}
$stack = $this->stackMapper->find($id);
$changes = new ChangeSet($stack);
$stack->setTitle($title);
$stack->setBoardId($boardId);
$stack->setOrder($order);
$stack->setDeletedAt($deletedAt);
return $this->stackMapper->update($stack);
$changes->setAfter($stack);
$stack = $this->stackMapper->update($stack);
$this->activityManager->triggerUpdateEvents(ActivityManager::DECK_OBJECT_BOARD, $changes, ActivityManager::SUBJECT_STACK_UPDATE);
return $stack;
}
/**

View File

@@ -23,6 +23,9 @@
use OCP\Util;
Util::addScript('activity', 'richObjectStringParser');
Util::addStyle('activity', 'style');
Util::addStyle('deck', '../js/build/vendor');
Util::addScript('deck', 'build/vendor');

View File

@@ -1,11 +1,11 @@
<div id="stack-add" ng-if="boardservice.canEdit() && checkCanEdit()">
<form class="ng-pristine ng-valid" ng-submit="createStack()">
<label for="new-stack-input" class="hidden-visually"><?php p($l->t('Add a new stack')); ?></label>
<label for="new-stack-input-<?php p($_['headerControlsId']); ?>" class="hidden-visually"><?php p($l->t('Add a new stack')); ?></label>
<input type="text" class="no-close" placeholder="<?php p($l->t('Add a new stack')); ?>"
ng-focus="status.addStack=true"
ng-blur="status.addStack=false"
ng-model="newStack.title" required
id="new-stack-input"
id="new-stack-input-<?php p($_['headerControlsId']); ?>"
maxlength="100" />
<button class="button-inline icon icon-add" ng-style="{'opacity':'{{status.addStack ? 1: 0.5}}'}" type="submit" title="<?php p($l->t('Submit')); ?>">
<span class="hidden-visually"><?php p($l->t('Submit')); ?></span>

View File

@@ -24,13 +24,13 @@
</div>
<div class="board-header-controls hidden">
<?php print_unescaped($this->inc('part.board.headerControls')); ?>
<?php print_unescaped($this->inc('part.board.headerControls', ['headerControlsId' => 'main'])); ?>
</div>
<div class="board-header-controls app-popover-menu-utils">
<button class="icon-more button"></button>
<div class="popovermenu hidden">
<div id="popover-controls">
<?php print_unescaped($this->inc('part.board.headerControls')); ?>
<?php print_unescaped($this->inc('part.board.headerControls', ['headerControlsId' => 'popover'])); ?>
</div>
</div>
</div>

View File

@@ -16,6 +16,8 @@
<li class="tabHeader" ng-class="{'selected': (params.tab==1)}" ui-sref="{tab: 1}"><a><?php p($l->t('Tags')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==2)}" ui-sref="{tab: 2}"><a><?php p($l->t('Deleted Stacks')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==3)}" ui-sref="{tab: 3}"><a><?php p($l->t('Deleted Cards')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==4)}" ui-sref="{tab: 4}"><a><?php p($l->t('Activity')); ?></a></li>
</ul>
<div class="tabsContainer">
<div id="tabBoardShare" class="tab" ng-if="params.tab==0 || !params.tab">
@@ -147,4 +149,10 @@
</li>
</ul>
</div>
<div id="board-detail-activity" class="tab activityTabView" ng-if="params.tab==4">
<activity-component ng-if="boardservice.getCurrent()" type="deck_board" element="boardservice.getCurrent()"></activity-component>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<ul class="activities" infinite-scroll="$ctrl.page()" infinite-scroll-container="'#app-sidebar'" infinite-scroll-disabled="$ctrl.activityservice.running" infinite-scroll-immediate-check="false">
<li ng-if="$ctrl.loadingNewer()"><div class="icon-loading-small"></div></li>
<li class="activity box" ng-repeat="activity in $ctrl.getData($ctrl.element.id) track by $index">
<div class="activity-icon">
<img src="{{activity.icon}}" alt="">
</div>
<div class="activitysubject"
bind-html-compile="$ctrl.parseMessage(activity.subject_rich[0], activity.subject_rich[1])"></div>
<span class="activitytime has-tooltip live-relative-timestamp"
data-timestamp="{{ activity.timestamp }}">{{ activity.timestamp/1000 | relativeDateFilter }}</span>
<div class="activitymessage" ng-bind-html="activity.message"></div>
</li>
<li ng-if="$ctrl.loading"><div class="icon-loading-small"></div></li>
</ul>

View File

@@ -90,8 +90,10 @@
<div class="section-header-tabbed">
<ul class="tabHeaders ng-scope">
<li class="tabHeader selected" ng-class="{'selected': (params.tab==0 || !params.tab)}" ui-sref="{tab: 0}"><a><?php p($l->t('Description')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==0 || !params.tab)}" ui-sref="{tab: 0}"><a><?php p($l->t('Description')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==1)}" ui-sref="{tab: 1}"><a><?php p($l->t('Attachments')); ?></a></li>
<li class="tabHeader" ng-class="{'selected': (params.tab==2)}" ui-sref="{tab: 2}"><a><?php p($l->t('Activity')); ?></a></li>
</ul>
<div class="tabDetails">
<span class="save-indicator saved"><?php p($l->t('Saved')); ?></span>
@@ -128,4 +130,10 @@
ng-if="!description()"><?php p($l->t('Add a card description…')); ?></div>
</div>
</div>
<div class="section-content card-activity activityTabView" ng-if="params.tab === 2">
<activity-component type="deck_card" element="cardservice.getCurrent()"></activity-component>
</div>
</div>

View File

@@ -41,7 +41,7 @@
<div class="colorselect" ng-controller="ColorPickerController">
<div class="color" ng-repeat="c in ::colors" ng-style="{'background-color':'#{{ c }}'}" ng-click="b=setColor(b,c)" ng-class="{'selected': (c == b.color) }"></div>
<label class="colorselect-label{{ b.color | iconWhiteFilter }} color" ng-style="getCustomBackground(b.hashedColor)" ng-init="b.hashedColor='#' + b.color">
<input class="color" type="color" ng-model="b.hashedColor" value="#{{b.color}}" ng-change="b=setHashedColor(b)"/>
<input class="color" type="color" ng-model="b.hashedColor" ng-value="colorValue(b.color)" ng-change="b=setHashedColor(b)"/>
</label>
</div>
</div>
@@ -59,7 +59,7 @@
<div class="colorselect" ng-controller="ColorPickerController">
<div class="color" ng-repeat="c in ::colors" ng-style="{'background-color':'#{{ c }}'}" ng-click="selectColor(c);newBoard=setColor(newBoard,c)" ng-class="{'selected': (c == newBoard.color), 'dark': (newBoard.color | textColorFilter) === '#ffffff' }"><br /></div>
<label class="colorselect-label{{ newBoard.color | iconWhiteFilter }} color" ng-style="getCustomBackground(newBoard.hashedColor)" ng-init="newBoard.hashedColor='#' + newBoard.color">
<input class="color" type="color" ng-model="newBoard.hashedColor" value="#{{newBoard.color}}" ng-change="newBoard=setHashedColor(newBoard)"/>
<input class="color" type="color" ng-model="newBoard.hashedColor" ng-value="colorValue(newBoard.color)" ng-change="newBoard=setHashedColor(newBoard)"/>
</label>
</div>
</div>

View File

@@ -0,0 +1,344 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignedUsers;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Service\PermissionService;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
use OCP\IL10N;
use OCP\IUser;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
class ActivityManagerTest extends TestCase {
/** @var ActivityManager */
private $activityManager;
/** @var IManager|MockObject */
private $manager;
/** @var PermissionService|MockObject */
private $permissionService;
/** @var BoardMapper|MockObject */
private $boardMapper;
/** @var CardMapper|MockObject */
private $cardMapper;
/** @var StackMapper|MockObject */
private $stackMapper;
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var AclMapper|MockObject */
private $aclMapper;
/** @var IL10N|MockObject */
private $l10n;
/** @var string */
private $userId = 'admin';
public function setUp() {
$this->manager = $this->createMock(IManager::class);
$this->permissionService = $this->createMock(PermissionService::class);
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->stackMapper = $this->createMock(StackMapper::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->aclMapper = $this->createMock(AclMapper::class);
$this->l10n = $this->createMock(IL10N::class);
$this->activityManager = new ActivityManager(
$this->manager,
$this->permissionService,
$this->boardMapper,
$this->cardMapper,
$this->stackMapper,
$this->attachmentMapper,
$this->aclMapper,
$this->l10n,
$this->userId
);
}
public function testGetActivityFormatOwn() {
$managerClass = new \ReflectionClass(ActivityManager::class);
$this->l10n->expects($this->any())
->method('t')
->will($this->returnCallback(function ($s) { return $s; }));
foreach ($managerClass->getConstants() as $constant => $value) {
if (strpos($constant, 'SUBJECT') === 0) {
$format = $this->activityManager->getActivityFormat($value, [], false);
if ($format !== '') {
$this->assertContains('{user}', $format);
} else {
/** @noinspection ForgottenDebugOutputInspection */
print_r('No activity string found for '. $constant . PHP_EOL);
}
$format = $this->activityManager->getActivityFormat($value, [], true);
if ($format !== '') {
$this->assertStringStartsWith('You', $format);
} else {
/** @noinspection ForgottenDebugOutputInspection */
print_r('No own activity string found for '. $constant . PHP_EOL);
}
}
}
}
public function testCreateEvent() {
$board = new Board();
$this->boardMapper->expects($this->once())
->method('find')
->willReturn($board);
$event = $this->createMock(IEvent::class);
$this->manager->expects($this->once())
->method('generateEvent')
->willReturn($event);
$event->expects($this->once())->method('setApp')->willReturn($event);
$event->expects($this->once())->method('setType')->willReturn($event);
$event->expects($this->once())->method('setAuthor')->willReturn($event);
$event->expects($this->once())->method('setObject')->willReturn($event);
$event->expects($this->once())->method('setSubject')->willReturn($event);
$event->expects($this->once())->method('setTimestamp')->willReturn($event);
$actual = $this->invokePrivate($this->activityManager, 'createEvent', [
ActivityManager::DECK_OBJECT_BOARD,
$board,
ActivityManager::SUBJECT_BOARD_CREATE
]);
$this->assertEquals($event, $actual);
}
public function dataSendToUsers() {
return [
[ActivityManager::DECK_OBJECT_BOARD],
[ActivityManager::DECK_OBJECT_CARD],
];
}
private function mockUser($uid) {
$user = $this->createMock(IUser::class);
$user->expects($this->any())
->method('getUID')
->willReturn($uid);
return $user;
}
/**
* @dataProvider dataSendToUsers
*/
public function testSendToUser($objectType) {
$users = [
$this->mockUser('user1'),
$this->mockUser('user2'),
];
$event = $this->createMock(IEvent::class);
$event->expects($this->at(0))
->method('getObjectType')
->willReturn($objectType);
$event->expects($this->at(0))
->method('getObjectId')
->willReturn(1);
$event->expects($this->at(2))
->method('setAffectedUser')
->with('user1');
$event->expects($this->at(3))
->method('setAffectedUser')
->with('user2');
$mapper = null;
switch ($objectType) {
case ActivityManager::DECK_OBJECT_BOARD:
$mapper = $this->boardMapper;
break;
case ActivityManager::DECK_OBJECT_CARD:
$mapper = $this->cardMapper;
break;
}
$mapper->expects($this->once())
->method('findBoardId')
->willReturn(123);
$this->permissionService->expects($this->once())
->method('findUsers')
->willReturn($users);
$this->manager->expects($this->at(0))
->method('publish')
->with($event);
$this->manager->expects($this->at(1))
->method('publish')
->with($event);
$this->invokePrivate($this->activityManager, 'sendToUsers', [$event]);
}
public function dataFindObjectForEntity() {
$board = new Board();
$board->setId(1);
$stack = new Stack();
$stack->setBoardId(1);
$card = new Card();
$card->setId(3);
$attachment = new Attachment();
$attachment->setCardId(3);
$label = new Label();
$label->setCardId(3);
$label->setBoardId(1);
$assignedUser = new AssignedUsers();
$assignedUser->setCardId(3);
return [
[ActivityManager::DECK_OBJECT_BOARD, $board],
[ActivityManager::DECK_OBJECT_BOARD, $stack],
[ActivityManager::DECK_OBJECT_BOARD, $label],
[ActivityManager::DECK_OBJECT_CARD, $card],
[ActivityManager::DECK_OBJECT_CARD, $attachment],
[ActivityManager::DECK_OBJECT_CARD, $assignedUser],
[ActivityManager::DECK_OBJECT_CARD, $label],
];
}
/**
* @param $objectType
* @param $entity
* @dataProvider dataFindObjectForEntity
*/
public function testFindObjectForEntity($objectType, $entity) {
$board = new Board();
$board->setId(1);
$card = new Card();
$card->setId(3);
$expected = null;
if ($objectType === ActivityManager::DECK_OBJECT_BOARD) {
$this->boardMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($board);
$expected = $board;
}
if ($objectType === ActivityManager::DECK_OBJECT_CARD) {
$this->cardMapper->expects($this->once())
->method('find')
->with(3)
->willReturn($card);
$expected = $card;
}
$actual = $this->invokePrivate($this->activityManager, 'findObjectForEntity', [$objectType, $entity]);
$this->assertEquals($expected, $actual);
}
public function testFindDetailsForStack() {
$stack = new Stack();
$stack->setId(123);
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([
'stack' => $stack,
'board' => $board
], $this->invokePrivate($this->activityManager, 'findDetailsForStack', [123]));
}
public function testFindDetailsForCard() {
$card = new Card();
$card->setId(555);
$card->setStackId(123);
$stack = new Stack();
$stack->setId(123);
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->cardMapper->expects($this->once())
->method('find')
->with(555)
->willReturn($card);
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([
'stack' => $stack,
'board' => $board,
'card' => $card
], $this->invokePrivate($this->activityManager, 'findDetailsForCard', [555]));
}
public function testFindDetailsForAttachment() {
$attachment = new Attachment();
$attachment->setId(777);
$attachment->setCardId(555);
$card = new Card();
$card->setId(555);
$card->setStackId(123);
$stack = new Stack();
$stack->setId(123);
$stack->setBoardId(999);
$board = new Board();
$board->setId(999);
$this->attachmentMapper->expects($this->once())
->method('find')
->with(777)
->willReturn($attachment);
$this->cardMapper->expects($this->once())
->method('find')
->with(555)
->willReturn($card);
$this->stackMapper->expects($this->once())
->method('find')
->with(123)
->willReturn($stack);
$this->boardMapper->expects($this->once())->method('find')
->with(999)
->willReturn($board);
$this->assertEquals([
'stack' => $stack,
'board' => $board,
'card' => $card,
'attachment' => $attachment
], $this->invokePrivate($this->activityManager, 'findDetailsForAttachment', [777]));
}
public function invokePrivate(&$object, $methodName, array $parameters = array())
{
$reflection = new \ReflectionClass(get_class($object));
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use PHPUnit\Framework\TestCase;
class ChangeSetTest extends TestCase {
public function setUp() {
}
public function testChangeSetScalar() {
$changeSet = new ChangeSet('A', 'B');
$this->assertEquals('A', $changeSet->getBefore());
$this->assertEquals('B', $changeSet->getAfter());
$this->assertFalse($changeSet->getDiff());
$changeSet->enableDiff();
$this->assertTrue($changeSet->getDiff());
}
public function testChangeSetObject() {
$a = new \stdClass;
$a->data = 'A';
$b = new \stdClass;
$b->data = 'B';
$changeSet = new ChangeSet($a, $b);
$this->assertEquals('A', $changeSet->getBefore()->data);
$this->assertEquals('B', $changeSet->getAfter()->data);
$this->assertNotSame($a, $changeSet->getBefore());
$this->assertNotSame($b, $changeSet->getAfter());
$this->assertFalse($changeSet->getDiff());
$changeSet->enableDiff();
$this->assertTrue($changeSet->getDiff());
}
}

View File

@@ -0,0 +1,454 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use OC\Activity\Event;
use OCA\Deck\Db\Acl;
use OCP\Activity\IEvent;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\RichObjectStrings\IValidator;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
class DeckProviderTest extends TestCase {
/** @var DeckProvider */
private $provider;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var ActivityManager|MockObject */
private $activityManager;
/** @var IUserManager|MockObject */
private $userManager;
/** @var string */
private $userId = 'admin';
public function setUp() {
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->provider = new DeckProvider($this->urlGenerator, $this->activityManager, $this->userManager, $this->userId);
}
private function mockEvent($objectType, $objectId, $objectName, $subject, $subjectParameters = []) {
$data = [];
$event = $this->createMock(IEvent::class);
$event->expects($this->any())->method('getApp')->willReturn('deck');
$event->expects($this->any())->method('getSubject')->willReturn($subject);
$event->expects($this->any())->method('getSubjectParameters')->willReturn($subjectParameters);
$event->expects($this->any())->method('getObjectType')->willReturn($objectType);
$event->expects($this->any())->method('getObjectId')->willReturn($objectId);
$event->expects($this->any())->method('getObjectName')->willReturn($objectName);
$event->expects($this->any())->method('getAuthor')->willReturn('admin');
$event->expects($this->any())->method('getMessage')->willReturn('');
$event->expects($this->any())->method('setIcon')->will($this->returnCallback(function($icon) use (&$data) {
$data['icon'] = $icon;
}));
$event->expects($this->any())->method('getIcon')->will(
$this->returnCallback(function() use (&$data) {
return array_key_exists('icon', $data) ? $data['icon'] : 'noicon';
})
);
return $event;
}
/**
* @expectedException \InvalidArgumentException
*/
public function testParseFailureApp() {
$event = $this->createMock(IEvent::class);
$event->expects($this->once())->method('getApp')->willReturn('notdeck');
$this->provider->parse('en_US', $event, $event);
}
public function dataEventIcons() {
return [
[ActivityManager::SUBJECT_LABEL_ASSIGN, 'deck', 'deck-dark.svg'],
[ActivityManager::SUBJECT_CARD_CREATE, 'files', 'add-color.svg'],
[ActivityManager::SUBJECT_CARD_UPDATE, 'files', 'change.svg'],
[ActivityManager::SUBJECT_CARD_DELETE, 'files', 'delete-color.svg'],
[ActivityManager::SUBJECT_CARD_UPDATE_ARCHIVE, 'deck', 'archive.svg'],
[ActivityManager::SUBJECT_CARD_RESTORE, 'core', 'actions/history.svg'],
[ActivityManager::SUBJECT_ATTACHMENT_UPDATE, 'core', 'places/files.svg'],
];
}
/**
* @dataProvider dataEventIcons
* @param $subject
* @param $icon
*/
public function testEventIcons($subject, $app, $icon) {
$event = $this->mockEvent(
ActivityManager::DECK_OBJECT_BOARD, 1, 'Board',
$subject);
$this->urlGenerator->expects($this->any())
->method('imagePath')
->will($this->returnCallback(function($a, $i) {
return $a . '/' . $i;
}));
$this->provider->parse('en_US', $event);
$this->assertEquals($app . '/' . $icon, $event->getIcon());
}
public function testDeckUrl() {
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('deck.page.index')
->willReturn('http://localhost/index.php/apps/deck/');
$this->assertEquals(
'http://localhost/index.php/apps/deck/#!board/1/card/1',
$this->provider->deckUrl('board/1/card/1')
);
}
public function testParseObjectTypeBoard() {
$this->urlGenerator->expects($this->any())
->method('imagePath')
->will($this->returnCallback(function($a, $i) {
return $a . '/' . $i;
}));
$this->activityManager->expects($this->once())
->method('getActivityFormat')
->willReturn('test string {board}');
$user = $this->createMock(IUser::class);
$user->expects($this->any())->method('getDisplayName')->willReturn('Administrator');
$this->userManager->expects($this->any())
->method('get')
->willReturn($user);
$richValidator = $this->createMock(IValidator::class);
$event = new Event($richValidator);
$event->setApp('deck');
$event->setSubject(ActivityManager::SUBJECT_BOARD_CREATE);
$event->setAffectedUser($this->userId);
$event->setAuthor($this->userId);
$event->setObject(ActivityManager::DECK_OBJECT_BOARD, 1, 'Board');
$this->provider->parse('en_US', $event);
$data = [
'board' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Board',
'link' => '#!/board/1',
],
'card' => null,
'user' => [
'type' => 'user',
'id' => 'admin',
'name' => 'Administrator',
]
];
$this->assertEquals($data, $event->getRichSubjectParameters());
}
public function testParseObjectTypeCard() {
$this->urlGenerator->expects($this->any())
->method('imagePath')
->will($this->returnCallback(function($a, $i) {
return $a . '/' . $i;
}));
$this->activityManager->expects($this->once())
->method('getActivityFormat')
->willReturn('test string {board}');
$user = $this->createMock(IUser::class);
$user->expects($this->any())->method('getDisplayName')->willReturn('Administrator');
$this->userManager->expects($this->any())
->method('get')
->willReturn($user);
$richValidator = $this->createMock(IValidator::class);
$event = new Event($richValidator);
$event->setApp('deck');
$event->setSubject(ActivityManager::SUBJECT_CARD_CREATE);
$event->setAffectedUser($this->userId);
$event->setAuthor($this->userId);
$event->setObject(ActivityManager::DECK_OBJECT_CARD, 1, 'Card');
$this->provider->parse('en_US', $event);
$data = [
'board' => null,
'card' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Card',
],
'user' => [
'type' => 'user',
'id' => 'admin',
'name' => 'Administrator',
]
];
$this->assertEquals($data, $event->getRichSubjectParameters());
$this->assertEquals('test string {board}', $event->getParsedSubject());
$this->assertEquals('test string {board}', $event->getRichSubject());
$this->assertEquals('', $event->getMessage());
}
public function testParseObjectTypeCardWithDiff() {
$this->urlGenerator->expects($this->any())
->method('imagePath')
->will($this->returnCallback(function($a, $i) {
return $a . '/' . $i;
}));
$this->activityManager->expects($this->once())
->method('getActivityFormat')
->willReturn('test string {board}');
$user = $this->createMock(IUser::class);
$user->expects($this->any())->method('getDisplayName')->willReturn('Administrator');
$this->userManager->expects($this->any())
->method('get')
->willReturn($user);
$richValidator = $this->createMock(IValidator::class);
$event = new Event($richValidator);
$event->setApp('deck');
$event->setSubject(ActivityManager::SUBJECT_CARD_UPDATE_DESCRIPTION, [
'before' => 'ABC',
'after' => 'BCD',
'diff' => true,
]);
$event->setAffectedUser($this->userId);
$event->setAuthor($this->userId);
$event->setObject(ActivityManager::DECK_OBJECT_CARD, 1, 'Card');
$event->setMessage('BCD');
$this->provider->parse('en_US', $event);
$data = [
'board' => null,
'card' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Card',
],
'user' => [
'type' => 'user',
'id' => 'admin',
'name' => 'Administrator',
],
];
$this->assertEquals($data, $event->getRichSubjectParameters());
$this->assertEquals('test string {board}', $event->getParsedSubject());
$this->assertEquals('test string {board}', $event->getRichSubject());
$this->assertEquals('BCD', $event->getMessage());
$this->assertEquals('<pre class="visualdiff"><del>A</del>BC<ins>D</ins></pre>', $event->getParsedMessage());
}
public function testParseParamForBoard() {
$params = [];
$subjectParams = [
'board' => [
'id' => 1,
'title' => 'Board name',
],
];
$expected = [
'board' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Board name',
'link' => '#!/board/1/',
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForBoard', ['board', $subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForStack() {
$params = [];
$subjectParams = [
'stack' => [
'id' => 1,
'title' => 'Stack name',
],
];
$expected = [
'stack' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Stack name',
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForStack', ['stack', $subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForAttachment() {
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->willReturn('/link/to/attachment');
$params = [];
$subjectParams = [
'attachment' => [
'id' => 1,
'data' => 'File name',
],
'card' => [
'id' => 1,
]
];
$expected = [
'attachment' => [
'type' => 'highlight',
'id' => 1,
'name' => 'File name',
'link' => '/link/to/attachment',
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForAttachment', ['attachment', $subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForAssignedUser() {
$user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getDisplayName')
->willReturn('User 1');
$this->userManager->expects($this->once())
->method('get')
->willReturn($user);
$params = [];
$subjectParams = [
'assigneduser' => 'user1',
];
$expected = [
'assigneduser' => [
'type' => 'user',
'id' => 'user1',
'name' => 'User 1'
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForAssignedUser', [$subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForLabel() {
$params = [];
$subjectParams = [
'label' => [
'id' => 1,
'title' => 'Label title',
],
];
$expected = [
'label' => [
'type' => 'highlight',
'id' => 1,
'name' => 'Label title'
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForLabel', [$subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForAclUser() {
$user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getDisplayName')
->willReturn('User 1');
$this->userManager->expects($this->once())
->method('get')
->willReturn($user);
$params = [];
$subjectParams = [
'acl' => [
'id' => 1,
'type' => Acl::PERMISSION_TYPE_USER,
'participant' => 'user1'
],
];
$expected = [
'acl' => [
'type' => 'user',
'id' => 'user1',
'name' => 'User 1'
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForAcl', [$subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForAclGroup() {
$params = [];
$subjectParams = [
'acl' => [
'id' => 1,
'type' => Acl::PERMISSION_TYPE_GROUP,
'participant' => 'group'
],
];
$expected = [
'acl' => [
'type' => 'highlight',
'id' => 'group',
'name' => 'group'
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForAcl', [$subjectParams, $params]);
$this->assertEquals($expected, $actual);
}
public function testParseParamForChanges() {
$event = $this->createMock(IEvent::class);
$params = [];
$subjectParams = [
'before' => 'ABC',
'after' => 'BCD'
];
$expected = [
'before' => [
'type' => 'highlight',
'id' => 'ABC',
'name' => 'ABC'
],
'after' => [
'type' => 'highlight',
'id' => 'BCD',
'name' => 'BCD'
],
];
$actual = $this->invokePrivate($this->provider, 'parseParamForChanges', [$subjectParams, $params, $event]);
$this->assertEquals($expected, $actual);
}
public function invokePrivate(&$object, $methodName, array $parameters = array())
{
$reflection = new \ReflectionClass(get_class($object));
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use OCP\IL10N;
use OCP\IURLGenerator;
use PHPUnit\Framework\TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
class FilterTest extends TestCase {
/** @var Filter */
private $filter;
/** @var IL10N|MockObject */
private $l10n;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
public function setUp() {
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->filter = new Filter($this->l10n, $this->urlGenerator);
}
public function testGetIdentifier() {
$this->assertEquals('deck', $this->filter->getIdentifier());
}
public function testGetName() {
$this->l10n->expects($this->once())
->method('t')
->with('Deck')
->willReturn('Deck');
$this->assertEquals('Deck', $this->filter->getName());
}
public function testGetPriority() {
$this->assertEquals(90, $this->filter->getPriority());
}
public function testGetIcon() {
$this->urlGenerator->expects($this->once())
->method('imagePath')
->with('deck', 'deck-dark.svg')
->willReturn('http://localhost/apps/deck/img/deck-dark.svg');
$this->assertEquals('http://localhost/apps/deck/img/deck-dark.svg', $this->filter->getIcon());
}
public function testFilterTypes() {
$data = ['deck_board', 'deck_card'];
$this->assertEquals($data, $this->filter->filterTypes($data));
}
public function testAllowedApps() {
$this->assertEquals(['deck'], $this->filter->allowedApps());
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* @copyright Copyright (c) 2018 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\Activity;
use PHPUnit\Framework\TestCase;
class SettingTest extends TestCase {
/** @var Setting */
private $setting;
public function setUp() {
$this->setting = new Setting();
}
public function testGetIdentifier() {
$this->assertEquals('deck', $this->setting->getIdentifier());
}
public function testGetName() {
$this->assertEquals('Deck', $this->setting->getName());
}
public function testGetPriority() {
$this->assertEquals(90, $this->setting->getPriority());
}
public function testCanChangeStream() {
$this->assertTrue($this->setting->canChangeStream());
}
public function testIsDefaultEnabledStream() {
$this->assertTrue($this->setting->isDefaultEnabledStream());
}
public function testCanChangeMail() {
$this->assertTrue($this->setting->canChangeMail());
}
public function testIsDefaultEnabledMail() {
$this->assertFalse($this->setting->isDefaultEnabledMail());
}
}

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
@@ -65,6 +66,8 @@ class AttachmentServiceTest extends TestCase {
private $attachmentService;
/** @var MockObject */
private $attachmentServiceImpl;
/** @var ActivityManager */
private $activityManager;
private $appContainer;
/** ICache */
private $cache;
@@ -85,7 +88,8 @@ class AttachmentServiceTest extends TestCase {
$this->permissionService = $this->createMock(PermissionService::class);
$this->application = $this->createMock(Application::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->cache = $this->createMock(ICache::class);
$this->cacheFactory->expects($this->any())->method('createDistributed')->willReturn($this->cache);
@@ -96,7 +100,7 @@ class AttachmentServiceTest extends TestCase {
$this->l10n = $this->createMock(IL10N::class);
$this->attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $this->application, $this->cacheFactory, $this->userId, $this->l10n);
$this->attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $this->application, $this->cacheFactory, $this->userId, $this->l10n, $this->activityManager);
}
public function testRegisterAttachmentService() {
@@ -108,7 +112,7 @@ class AttachmentServiceTest extends TestCase {
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId, $this->l10n);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId, $this->l10n, $this->activityManager);
$attachmentService->registerAttachmentService('custom', MyAttachmentService::class);
$this->assertEquals($fileServiceMock, $attachmentService->getService('deck_file'));
$this->assertEquals(MyAttachmentService::class, get_class($attachmentService->getService('custom')));
@@ -126,7 +130,7 @@ class AttachmentServiceTest extends TestCase {
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId, $this->l10n);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId, $this->l10n, $this->activityManager);
$attachmentService->registerAttachmentService('custom', MyAttachmentService::class);
$attachmentService->getService('deck_file_invalid');
}
@@ -367,4 +371,4 @@ class AttachmentServiceTest extends TestCase {
$actual = $this->attachmentService->restore(123, 1);
}
}
}

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Service;
use OC\L10N\L10N;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\AclMapper;
use OCA\Deck\Db\AssignedUsers;
@@ -59,8 +60,10 @@ class BoardServiceTest extends TestCase {
private $userManager;
/** @var IUserManager */
private $groupManager;
/** @var ActivityManager */
private $activityManager;
private $userId = 'admin';
private $userId = 'admin';
public function setUp() {
parent::setUp();
@@ -72,7 +75,8 @@ class BoardServiceTest extends TestCase {
$this->notificationHelper = $this->createMock(NotificationHelper::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->service = new BoardService(
$this->boardMapper,
@@ -84,11 +88,12 @@ class BoardServiceTest extends TestCase {
$this->assignedUsersMapper,
$this->userManager,
$this->groupManager,
$this->activityManager,
$this->userId
);
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('admin');
$user->method('getUID')->willReturn('admin');
}
public function testFindAll() {
@@ -186,7 +191,12 @@ class BoardServiceTest extends TestCase {
->willReturn([
'admin' => 'admin',
]);
$this->assertEquals($board, $this->service->delete(123));
$boardDeleted = clone $board;
$board->setDeletedAt(1);
$this->boardMapper->expects($this->once())
->method('update')
->willReturn($boardDeleted);
$this->assertEquals($boardDeleted, $this->service->delete(123));
}
public function testAddAcl() {
@@ -268,4 +278,4 @@ class BoardServiceTest extends TestCase {
->willReturn(true);
$this->assertTrue($this->service->deleteAcl(123));
}
}
}

View File

@@ -24,6 +24,7 @@
namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\AssignedUsers;
use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Card;
@@ -34,6 +35,7 @@ use OCA\Deck\Db\LabelMapper;
use OCA\Deck\NotFoundException;
use OCA\Deck\Notification\NotificationHelper;
use OCA\Deck\StatusException;
use OCP\Activity\IEvent;
use Test\TestCase;
class CardServiceTest extends TestCase {
@@ -55,7 +57,10 @@ class CardServiceTest extends TestCase {
/** @var LabelMapper|\PHPUnit\Framework\MockObject\MockObject */
private $labelMapper;
private $boardMapper;
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var ActivityManager|\PHPUnit\Framework\MockObject\MockObject */
private $activityManager;
public function setUp() {
parent::setUp();
@@ -68,7 +73,8 @@ class CardServiceTest extends TestCase {
$this->notificationHelper = $this->createMock(NotificationHelper::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->cardService = new CardService(
$this->activityManager = $this->createMock(ActivityManager::class);
$this->cardService = new CardService(
$this->cardMapper,
$this->stackMapper,
$this->boardMapper,
@@ -78,10 +84,23 @@ class CardServiceTest extends TestCase {
$this->notificationHelper,
$this->assignedUsersMapper,
$this->attachmentService,
$this->activityManager,
'user1'
);
}
public function mockActivity($type, $object, $subject) {
// ActivityManager::DECK_OBJECT_BOARD, $newAcl, ActivityManager::SUBJECT_BOARD_SHARE
$event = $this->createMock(IEvent::class);
$this->activityManager->expects($this->once())
->method('createEvent')
->with($type, $object, $subject)
->willReturn($event);
$this->activityManager->expects($this->once())
->method('sendToUsers')
->with($event);
}
public function testFind() {
$card = new Card();
$card->setId(1337);
@@ -263,7 +282,7 @@ class CardServiceTest extends TestCase {
$card = new Card();
$card->setArchived(true);
$this->cardMapper->expects($this->once())->method('find')->willReturn($card);
$this->cardMapper->expects($this->never())->method('removeLabel');
$this->cardMapper->expects($this->never())->method('removeLabel');
$this->expectException(StatusException::class);
$this->cardService->removeLabel(123, 999);
}
@@ -321,9 +340,9 @@ class CardServiceTest extends TestCase {
/**
* @expectException \OCA\Deck\NotFoundException
*
*
*
*
*
*
*/
public function testUnassignUserNotExisting() {
$assignment = new AssignedUsers();
@@ -337,7 +356,7 @@ class CardServiceTest extends TestCase {
->with(123)
->willReturn($assignments);
$this->expectException(NotFoundException::class);
$actual = $this->cardService->unassignUser(123, 'user');
$actual = $this->cardService->unassignUser(123, 'user');
}

View File

@@ -25,6 +25,7 @@ namespace OCA\Deck\Service;
use OCA\Deck\Activity\ActivityManager;
use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
@@ -63,6 +64,8 @@ class StackServiceTest extends TestCase {
private $boardService;
/** @var CardService|\PHPUnit\Framework\MockObject\MockObject */
private $cardService;
/** @var ActivityManager|\PHPUnit\Framework\MockObject\MockObject */
private $activityManager;
public function setUp() {
parent::setUp();
@@ -75,18 +78,19 @@ class StackServiceTest extends TestCase {
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->labelMapper = $this->createMock(LabelMapper::class);
$this->activityManager = $this->createMock(ActivityManager::class);
$this->stackService = new StackService(
$this->stackMapper,
$this->boardMapper,
$this->cardMapper,
$this->labelMapper,
$this->boardMapper,
$this->cardMapper,
$this->labelMapper,
$this->permissionService,
$this->boardService,
$this->cardService,
$this->assignedUsersMapper,
$this->attachmentService
$this->attachmentService,
$this->activityManager
);
}
@@ -176,14 +180,16 @@ class StackServiceTest extends TestCase {
}
public function testDelete() {
$this->permissionService->expects($this->once())->method('checkPermission');
$stackToBeDeleted = new Stack();
$stackToBeDeleted->setId(1);
$this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted);
$this->stackMapper->expects($this->once())->method('update');
$this->stackService->delete(123);
$this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), "deletedAt is in the past");
}
$this->permissionService->expects($this->once())->method('checkPermission');
$stackToBeDeleted = new Stack();
$stackToBeDeleted->setId(1);
$this->stackMapper->expects($this->once())->method('find')->willReturn($stackToBeDeleted);
$this->stackMapper->expects($this->once())->method('update')->willReturn($stackToBeDeleted);
$this->stackService->delete(123);
$this->assertTrue($stackToBeDeleted->getDeletedAt() <= time(), "deletedAt is in the past");
$this->assertTrue($stackToBeDeleted->getDeletedAt() > 0, "deletedAt is set");
}
public function testUpdate() {
$this->permissionService->expects($this->once())->method('checkPermission');