Merge pull request #488 from nextcloud/feature/109/file-attachments

File attachments for cards
This commit is contained in:
Julius Härtl
2018-06-20 08:34:57 +02:00
committed by GitHub
58 changed files with 2662 additions and 190 deletions

View File

@@ -33,6 +33,7 @@ rules:
no-fallthrough: error
no-mixed-spaces-and-tabs: error
no-unused-vars: warn
no-useless-escape: warn
no-use-before-define: error
semi: ["error", "always"]
indent:

View File

@@ -198,12 +198,6 @@
<autoincrement>1</autoincrement>
<length>4</length>
</field>
<field>
<name>title</name>
<type>text</type>
<notnull>true</notnull>
<length>100</length>
</field>
<field>
<name>card_id</name>
<type>integer</type>
@@ -218,12 +212,12 @@
</field>
<field>
<name>data</name>
<type>clob</type>
<type>text</type>
</field>
<field>
<name>last_modified</name>
<type>integer</type>
<default></default>
<default/>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
@@ -231,7 +225,21 @@
<field>
<name>created_at</name>
<type>integer</type>
<default></default>
<default/>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>
</field>
<field>
<name>created_by</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
</field>
<field>
<name>deleted_at</name>
<type>integer</type>
<default>0</default>
<length>8</length>
<notnull>false</notnull>
<unsigned>true</unsigned>

View File

@@ -59,6 +59,16 @@ return [
['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'],
['name' => 'card#unassignUser', 'url' => '/cards/{cardId}/assign/{userId}', 'verb' => 'DELETE'],
['name' => 'attachment#getAll', 'url' => '/cards/{cardId}/attachments', 'verb' => 'GET'],
['name' => 'attachment#create', 'url' => '/cards/{cardId}/attachment', 'verb' => 'POST'],
['name' => 'attachment#display', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'GET'],
['name' => 'attachment#update', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'PUT'],
// also allow to use POST for updates so we can properly access files when using application/x-www-form-urlencoded
['name' => 'attachment#update', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'POST'],
['name' => 'attachment#delete', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'DELETE'],
['name' => 'attachment#restore', 'url' => '/cards/{cardId}/attachment/{attachmentId}/restore', 'verb' => 'GET'],
// labels
['name' => 'label#create', 'url' => '/labels', 'verb' => 'POST'],
['name' => 'label#update', 'url' => '/labels/{labelId}', 'verb' => 'PUT'],

View File

@@ -143,6 +143,10 @@ input.input-inline {
.card {
opacity: 1;
&.file-drop {
}
}
&.card-selected {
@@ -261,10 +265,6 @@ input.input-inline {
margin-right: 6px;
}
}
> button {
padding: 16px 20px;
}
}
.filter-select {
@@ -461,7 +461,7 @@ input.input-inline {
}
}
.card-tasks {
.card-tasks, .card-files {
border-radius: 3px;
margin: 4px 4px 4px 0px;
padding: 0 2px;
@@ -472,6 +472,7 @@ input.input-inline {
.icon {
background-size: contain;
margin-right: 2px;
}
}
@@ -653,6 +654,32 @@ input.input-inline {
}
}
.drop-indicator {
display: none;
}
.card .nv-file-over,
.drop-indicator.nv-file-over {
display: block;
position: absolute;
width: 100%;
height: 100%;
background-color: #fff;
z-index: 100;
opacity: 0.9;
text-align: center;
p {
width: calc(100% - 20px);
height: calc(100% - 20px);
position: absolute;
padding: 20px;
border: 1px dashed #AAA;
margin: 10px;
border-radius: 5px;
}
}
#card-meta { // TODO: use .card-block instead?
height: 100%;
display: flex;
@@ -696,10 +723,23 @@ input.input-inline {
flex: 1;
}
}
.section-header-tabbed {
margin-top: 10px;
margin-bottom: 5px;
flex-shrink: 0;
display: flex;
.tabHeaders {
margin: 0;
flex-grow: 1;
}
.tabDetails {
display: flex;
}
}
.save-indicator {
border-radius: 3px;
float: right;
margin: 5px;
padding: 0 10px;
font-size: 8pt;
display: none;
@@ -750,6 +790,97 @@ input.input-inline {
}
}
.icon-upload.icon-loading-small {
background-image: none;
}
.attachment-list-wrapper {
position: fixed;
width: 100%;
height: 100%;
background-color: rgba($color-darkgrey, 0.5);
left: 0;
top: 0;
}
.attachment-list {
&.selector {
padding: 10px;
position: absolute;
width: 30%;
max-width: 500px;
min-width: 200px;
max-height: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: $color-main-background;
z-index: 2;
border-radius: 3px;
box-shadow: 0 0 3px $color-darkgrey;
overflow: scroll;
}
h3.attachment-selector {
margin: 0 0 10px;
padding: 0;
.icon-close {
display: inline-block;
float: right;
}
}
li.attachment {
display: flex;
padding: 3px;
&.deleted {
opacity: .5;
}
.fileicon {
display: inline-block;
min-width: 32px;
width: 32px;
height: 32px;
background-size: contain;
}
.details {
flex-grow: 1;
flex-shrink: 1;
min-width: 0;
flex-basis: 50%;
line-height: 110%;
padding: 2px;
}
.filename {
width: 70%;
display: flex;
.basename {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 2px;
}
.extension {
opacity: 0.7;
}
}
.filesize, .filedate {
font-size: 90%;
color: $color-darkgrey;
}
.app-popover-menu-utils {
position: relative;
right: -10px;
button {
height: 32px;
width: 42px;
}
}
button.icon-history {
width: 44px;
}
}
}
.card-description {
&.section-header {
.save-indicator {
@@ -1137,7 +1268,11 @@ input.input-inline {
border: 0 !important;
overflow: hidden;
}
.select2-search-field {
margin-right: -10px;
}
}
.select2-choice {
height: auto;
}
@@ -1234,6 +1369,13 @@ input.input-inline {
}
}
img {
max-width: 100%;
max-height: 50vh;
margin: auto;
display: block;
}
input[type=checkbox] {
margin: 0px 10px 0px 0px;
line-height: 10px;

View File

@@ -47,12 +47,14 @@ import angularuiselect from 'ui-select';
import ngsortable from 'ng-sortable';
import md from 'angular-markdown-it';
import nganimate from 'angular-animate';
import 'angular-file-upload';
var app = angular.module('Deck', [
ngsanitize,
uirouter,
angularuiselect,
ngsortable, md, nganimate
ngsortable, md, nganimate,
'angularFileUpload'
]);
export default app;

View File

@@ -74,6 +74,9 @@ app.config(function ($provide, $interpolateProvider, $httpProvider, $urlRouterPr
})
.state('board.card', {
url: '/card/:cardId',
params: {
tab: {value: 0, dynamic: true},
},
views: {
'sidebarView': {
templateUrl: '/card.sidebarView.html',
@@ -82,4 +85,28 @@ app.config(function ($provide, $interpolateProvider, $httpProvider, $urlRouterPr
}
});
$provide.decorator('nvFileOverDirective', function ($delegate) {
var directive = $delegate[0],
link = directive.link;
directive.compile = function () {
return function (scope, element, attrs) {
var overClass = attrs.overClass || 'nv-file-over';
link.apply(this, arguments);
let counter = 0;
element.on('dragenter', function (event) {
counter++;
});
element.on('dragleave', function (event) {
counter--;
if (counter <= 0) {
$('.' + overClass).removeClass(overClass);
}
});
};
};
return $delegate;
});
});

View File

@@ -24,6 +24,7 @@ import app from './App.js';
/* global Snap */
app.run(function ($document, $rootScope, $transitions, BoardService) {
'use strict';
$document.click(function (event) {
$rootScope.$broadcast('documentClicked', event);
});

View File

@@ -0,0 +1,78 @@
/*
* @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 */
class AttachmentListController {
constructor ($scope, CardService, FileService) {
'ngInject';
this.cardservice = CardService;
this.fileservice = FileService;
this.attachments = CardService.getCurrent().attachments;
}
mimetypeForAttachment(attachment) {
let url = OC.MimeType.getIconUrl(attachment.extendedData.mimetype);
let styles = {
'background-image': `url("${url}")`,
};
return styles;
}
attachmentUrl(attachment) {
let cardId = this.cardservice.getCurrent().id;
let attachmentId = attachment.id;
return OC.generateUrl(`/apps/deck/cards/${cardId}/attachment/${attachmentId}`);
}
getAttachmentMarkdown(attachment) {
const inlineMimetypes = ['image/png', 'image/jpg', 'image/jpeg'];
let url = this.attachmentUrl(attachment);
let filename = attachment.data;
let insertText = `[📎 ${filename}](${url})`;
if (inlineMimetypes.indexOf(attachment.extendedData.mimetype) > -1) {
insertText = `![📎 ${filename}](${url})`;
}
return insertText;
}
select(attachment) {
this.onSelect({attachment: this.getAttachmentMarkdown(attachment)});
}
abort() {
this.onAbort();
}
}
let attachmentListComponent = {
templateUrl: '/card.attachments.html',
controller: AttachmentListController,
bindings: {
isFileSelector: '<',
attachments: '=',
onSelect: '&',
onAbort: '&'
}
};
export default attachmentListComponent;

View File

@@ -22,7 +22,7 @@
import app from '../app/App.js';
/* global oc_defaults OC */
app.controller('BoardController', function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService, LabelService, $state, $transitions, $filter) {
app.controller('BoardController', function ($rootScope, $scope, $stateParams, StatusService, BoardService, StackService, CardService, LabelService, $state, $transitions, $filter, FileService) {
$scope.sidebar = $rootScope.sidebar;
@@ -40,6 +40,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
$scope.labelservice = LabelService;
$scope.defaultColors = ['31CC7C', '317CCC', 'FF7A66', 'F1DB50', '7C31CC', 'CC317C', '3A3B3D', 'CACBCD'];
$scope.board = BoardService.getCurrent();
$scope.uploader = FileService.uploader;
// workaround for $stateParams changes not being propagated
$scope.$watch(function() {
@@ -47,7 +48,7 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
}, function (params) {
$scope.params = params;
}, true);
$scope.params = $state;
$scope.params = $state.params;
/**
* Check for markdown checkboxes in description to render the counter
@@ -353,4 +354,11 @@ app.controller('BoardController', function ($rootScope, $scope, $stateParams, St
};
};
$scope.attachmentCount = function(card) {
if (Array.isArray(card.attachments)) {
return card.attachments.filter((obj) => obj.deletedAt === 0).length;
}
return card.attachmentCount;
};
});

View File

@@ -20,10 +20,10 @@
*
*/
/* global app moment */
/* global app moment angular OC */
import app from '../app/App.js';
app.controller('CardController', function ($scope, $rootScope, $sce, $location, $stateParams, $interval, $timeout, $filter, BoardService, CardService, StackService, StatusService, markdownItConverter) {
app.controller('CardController', function ($scope, $rootScope, $sce, $location, $stateParams, $state, $interval, $timeout, $filter, BoardService, CardService, StackService, StatusService, markdownItConverter, FileService) {
$scope.sidebar = $rootScope.sidebar;
$scope.status = {
lastEdit: 0,
@@ -31,11 +31,44 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
};
$scope.cardservice = CardService;
$scope.fileservice = FileService;
$scope.cardId = $stateParams.cardId;
$scope.statusservice = StatusService.getInstance();
$scope.boardservice = BoardService;
$scope.isArray = angular.isArray;
// workaround for $stateParams changes not being propagated
$scope.$watch(function() {
return $state.params;
}, function (params) {
$scope.params = params;
}, true);
$scope.params = $state.params;
$scope.addAttachmentToDescription = function(insertText) {
let el = document.querySelectorAll('textarea')[0];
let start = el.selectionStart;
let end = el.selectionEnd;
let text = $scope.status.edit.description || '';
let before = text.substring(0, start);
let after = text.substring(end, text.length);
let newText = before + '\n' + insertText + '\n' + after;
$scope.status.edit.description = newText;
el.selectionStart = el.selectionEnd = start + newText.length;
el.focus();
$scope.status.continueEdit = false;
$scope.cardEditDescriptionChanged();
$scope.status.selectAttachment = false;
};
$scope.abortAttachmentSelection = function() {
$scope.status.continueEdit = false;
$scope.status.selectAttachment = false;
let el = document.querySelectorAll('textarea')[0];
el.focus();
};
$scope.statusservice.retainWaiting();
$scope.description = function() {
@@ -68,8 +101,8 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
var reg = /\[(X|\s|\_|\-)\]\s(.*)/ig;
var nth = 0;
$scope.status.edit.description = $scope.status.edit.description.replace(reg, function (match, i, original) {
var result = match;
if (nth++ === id) {
var result;
if (match.match(/^\[\s\]/i)) {
result = match.replace(/\[\s\]/i, '[x]');
}
@@ -81,10 +114,9 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
return match;
});
CardService.update($scope.status.edit).then(function (data) {
var header = $('.section-header.card-description');
var header = $('.section-header-tabbed .tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
StackService.updateCard($scope.status.edit);
});
$('#markdown input[type=checkbox]').removeAttr('disabled');
@@ -111,7 +143,7 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
};
$scope.cardEditDescriptionChanged = function ($event) {
$scope.status.lastEdit = Date.now();
var header = $('.section-header.card-description');
var header = $('.section-header-tabbed .tabDetails');
header.find('.save-indicator.unsaved').show();
header.find('.save-indicator.saved').hide();
};
@@ -121,13 +153,12 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
if (timeSinceEdit > 1000 && $scope.status.lastEdit > $scope.status.lastSave && !$scope.status.saving) {
$scope.status.lastSave = currentTime;
$scope.status.saving = true;
var header = $('.section-header.card-description');
var header = $('.section-header-tabbed .tabDetails');
header.find('.save-indicator.unsaved').fadeIn(500);
CardService.update($scope.status.edit).then(function (data) {
var header = $('.section-header.card-description');
var header = $('.section-header-tabbed .tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
StackService.updateCard($scope.status.edit);
$scope.status.saving = false;
});
}
@@ -136,29 +167,26 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
// handle rename to update information on the board as well
$scope.cardRename = function (card) {
CardService.rename(card).then(function (data) {
StackService.updateCard(card);
$scope.status.renameCard = false;
});
};
$scope.cardUpdate = function (card) {
CardService.update(card).then(function (data) {
$scope.status.cardEditDescription = false;
var header = $('.section-content.card-description');
$scope.updateMarkdown($scope.status.edit.description);
var header = $('.section-header-tabbed .tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(500).fadeOut(1000);
StackService.updateCard(card);
});
};
$scope.labelAssign = function (element, model) {
CardService.assignLabel($scope.cardId, element.id).then(function (data) {
StackService.updateCard(CardService.getCurrent());
});
};
$scope.labelRemove = function (element, model) {
CardService.removeLabel($scope.cardId, element.id).then(function (data) {
StackService.updateCard(CardService.getCurrent());
});
};
@@ -173,7 +201,6 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
newDate.year(duedate.year());
element.duedate = newDate.toISOString();
CardService.update(element);
StackService.updateCard(element);
};
$scope.setDuedateTime = function (time) {
var element = CardService.getCurrent();
@@ -185,14 +212,12 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
newDate.minute(time.minute());
element.duedate = newDate.toISOString();
CardService.update(element);
StackService.updateCard(element);
};
$scope.resetDuedate = function () {
var element = CardService.getCurrent();
element.duedate = null;
CardService.update(element);
StackService.updateCard(element);
};
/**
@@ -216,14 +241,12 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
$scope.addAssignedUser = function(item) {
CardService.assignUser(CardService.getCurrent(), item.uid).then(function (data) {
StackService.updateCard(CardService.getCurrent());
});
$scope.status.showAssignUser = false;
};
$scope.removeAssignedUser = function(uid) {
CardService.unassignUser(CardService.getCurrent(), uid).then(function (data) {
StackService.updateCard(CardService.getCurrent());
});
};

37
js/filters/bytesFilter.js Normal file
View File

@@ -0,0 +1,37 @@
/*
* @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.filter('bytes', function () {
return function (bytes, precision) {
if (isNaN(parseFloat(bytes, 10)) || !isFinite(bytes)) {
return '-';
}
if (typeof precision === 'undefined') {
precision = 2;
}
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'],
number = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
};
});

View File

@@ -13,7 +13,10 @@ import './app/Run.js';
import ListController from 'controller/ListController.js';
import attachmentListComponent from './controller/AttachmentController.js';
app.controller('ListController', ListController);
app.component('attachmentListComponent', attachmentListComponent);
// require all the js files from subdirectories

107
js/package-lock.json generated
View File

@@ -317,7 +317,6 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz",
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==",
"dev": true,
"optional": true,
"requires": {
"es6-promisify": "^5.0.0"
}
@@ -405,6 +404,11 @@
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.7.2.tgz",
"integrity": "sha512-/MQL2FEFXhdzFUKJ9AJq6gAYz3YGsh2xHTztuQKY5FkqjEBpF5YGypgprTz153P7t51T8nFqLsG8jwkpRdM6gg=="
},
"angular-file-upload": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/angular-file-upload/-/angular-file-upload-2.5.0.tgz",
"integrity": "sha1-D1PJP7xw7YuoNAqaKJumS2gOIWc="
},
"angular-markdown-it": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/angular-markdown-it/-/angular-markdown-it-0.6.1.tgz",
@@ -1201,8 +1205,7 @@
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-0.0.2.tgz",
"integrity": "sha1-JrOIXRD6E9t/wBquOquHAZngEkw=",
"dev": true,
"optional": true
"dev": true
},
"buffer-xor": {
"version": "1.0.3",
@@ -2321,15 +2324,13 @@
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
"dev": true,
"optional": true
"dev": true
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"dev": true,
"optional": true,
"requires": {
"es6-promise": "^4.0.3"
}
@@ -2899,8 +2900,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@@ -2921,14 +2921,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"
@@ -2943,20 +2941,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",
@@ -3073,8 +3068,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@@ -3086,7 +3080,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3101,7 +3094,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -3109,14 +3101,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"
@@ -3135,7 +3125,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -3216,8 +3205,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -3229,7 +3217,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -3315,8 +3302,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3352,7 +3338,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",
@@ -3372,7 +3357,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3416,14 +3400,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
}
}
},
@@ -3880,7 +3862,6 @@
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
"dev": true,
"optional": true,
"requires": {
"agent-base": "4",
"debug": "3.1.0"
@@ -3891,7 +3872,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"optional": true,
"requires": {
"ms": "2.0.0"
}
@@ -3914,18 +3894,24 @@
"resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
"integrity": "sha1-rQFScUOi6Hc8+uapb1hla7UqNLI=",
"dev": true,
"optional": true,
"requires": {
"httpreq": ">=0.4.22",
"underscore": "~1.7.0"
},
"dependencies": {
"underscore": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=",
"dev": true
}
}
},
"httpreq": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz",
"integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8=",
"dev": true,
"optional": true
"dev": true
},
"https-browserify": {
"version": "1.0.0",
@@ -3938,7 +3924,6 @@
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
"dev": true,
"optional": true,
"requires": {
"agent-base": "^4.1.0",
"debug": "^3.1.0"
@@ -3949,7 +3934,6 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"optional": true,
"requires": {
"ms": "2.0.0"
}
@@ -4226,8 +4210,7 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
"dev": true,
"optional": true
"dev": true
},
"is-absolute-url": {
"version": "2.1.0",
@@ -4657,15 +4640,13 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz",
"integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY=",
"dev": true,
"optional": true
"dev": true
},
"libmime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-3.0.0.tgz",
"integrity": "sha1-UaGp50SOy9Ms2lRCFnW7IbwJPaY=",
"dev": true,
"optional": true,
"requires": {
"iconv-lite": "0.4.15",
"libbase64": "0.1.0",
@@ -4676,8 +4657,7 @@
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
"integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=",
"dev": true,
"optional": true
"dev": true
}
}
},
@@ -4685,8 +4665,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz",
"integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=",
"dev": true,
"optional": true
"dev": true
},
"linkify-it": {
"version": "2.0.3",
@@ -5503,15 +5482,13 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz",
"integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q=",
"dev": true,
"optional": true
"dev": true
},
"nodemailer-shared": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz",
"integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=",
"dev": true,
"optional": true,
"requires": {
"nodemailer-fetch": "1.6.0"
}
@@ -5544,8 +5521,7 @@
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz",
"integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U=",
"dev": true,
"optional": true
"dev": true
},
"nopt": {
"version": "3.0.6",
@@ -6624,8 +6600,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
"dev": true,
"optional": true
"dev": true
},
"prepend-http": {
"version": "1.0.4",
@@ -7386,15 +7361,13 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz",
"integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=",
"dev": true,
"optional": true
"dev": true
},
"smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
"integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=",
"dev": true,
"optional": true,
"requires": {
"httpntlm": "1.6.1",
"nodemailer-shared": "1.1.0"
@@ -7591,7 +7564,6 @@
"resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz",
"integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=",
"dev": true,
"optional": true,
"requires": {
"ip": "^1.1.4",
"smart-buffer": "^1.0.13"
@@ -7602,7 +7574,6 @@
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz",
"integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==",
"dev": true,
"optional": true,
"requires": {
"agent-base": "^4.1.0",
"socks": "^1.1.10"
@@ -8127,7 +8098,6 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"dev": true,
"optional": true,
"requires": {
"prelude-ls": "~1.1.2"
}
@@ -8212,13 +8182,6 @@
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true
},
"underscore": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz",
"integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=",
"dev": true,
"optional": true
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",

View File

@@ -10,6 +10,7 @@
"dependencies": {
"angular": "^1.7.2",
"angular-animate": "^1.7.2",
"angular-file-upload": "^2.5.0",
"angular-markdown-it": "^0.6.1",
"angular-sanitize": "^1.7.2",
"markdown-it": "^8.4.1",

View File

@@ -125,7 +125,7 @@ app.factory('ApiService', function ($http, $q) {
this.data[entity.id] = entity;
} else {
Object.keys(entity).forEach(function (key) {
if (entity[key] !== null) {
if (entity[key] !== null && element[key] !== entity[key]) {
element[key] = entity[key];
}
});
@@ -163,6 +163,10 @@ app.factory('ApiService', function ($http, $q) {
return this.data;
};
ApiService.prototype.get = function (id) {
return this.data[id];
};
ApiService.prototype.getName = function () {
var funcNameRegex = /function (.{1,})\(/;
var results = (funcNameRegex).exec((this).constructor.toString());

View File

@@ -198,13 +198,13 @@ app.factory('BoardService', function (ApiService, $http, $q) {
return [];
}
var result = [this.getCurrent().owner];
this.getCurrent().users = [this.getCurrent().owner];
let self = this;
angular.forEach(this.getCurrent().acl, function(value, key) {
if (value.type === OC.Share.SHARE_TYPE_USER) {
result.push(value.participant);
self.getCurrent().users.push(value.participant);
}
});
this.getCurrent().users = result;
};
BoardService.prototype.getUsers = function () {

View File

@@ -133,6 +133,45 @@ app.factory('CardService', function (ApiService, $http, $q) {
return deferred.promise;
};
CardService.prototype.attachmentRemove = function (attachment) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id, {}).then(function (response) {
if (response.data.deletedAt > 0) {
let currentAttachment = self.getCurrent().attachments.find(function (obj) {
if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
} else {
self.getCurrent().attachments = self.getCurrent().attachments.filter(function (obj) {
return obj.id !== attachment.id;
});
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error when removing the attachment');
});
return deferred.promise;
};
CardService.prototype.attachmentRemoveUndo = function (attachment) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id + '/restore', {}).then(function (response) {
let currentAttachment = self.getCurrent().attachments.find(function (obj) {
if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error when restoring the attachment');
});
return deferred.promise;
};
var service = new CardService($http, 'cards', $q);
return service;
});

100
js/service/FileService.js Normal file
View File

@@ -0,0 +1,100 @@
/*
* @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';
/* global OC oc_requesttoken */
export default class FileService {
constructor ($http, FileUploader, CardService) {
this.uploader = new FileUploader();
this.cardservice = CardService;
this.uploader.onAfterAddingFile = this.onAfterAddingFile.bind(this);
this.uploader.onSuccessItem = this.onSuccessItem.bind(this);
}
runUpload (fileItem, attachmentId) {
fileItem.url = OC.generateUrl('/apps/deck/cards/' + fileItem.cardId + '/attachment');
if (typeof attachmentId !== 'undefined') {
fileItem.url = OC.generateUrl('/apps/deck/cards/' + fileItem.cardId + '/attachment/' + attachmentId);
} else {
fileItem.formData = [
{
requesttoken: oc_requesttoken,
type: 'deck_file',
}
];
}
fileItem.headers = {requesttoken: oc_requesttoken};
this.uploader.uploadItem(fileItem);
}
onAfterAddingFile (fileItem) {
// Fetch card details before trying to upload so we can detect filename collisions properly
let self = this;
this.cardservice.fetchOne(fileItem.cardId).then(function (data) {
let attachments = self.cardservice.get(fileItem.cardId).attachments;
let existingFile = attachments.find((attachment) => {
return attachment.data === fileItem.file.name;
});
if (typeof existingFile !== 'undefined') {
OC.dialogs.confirm(
`A file with the name ${fileItem.file.name} already exists. Do you want to overwrite it?`,
'File already exists',
function (result) {
if (result) {
self.runUpload(fileItem, existingFile.id);
} else {
let fileName = existingFile.extendedData.info.filename;
let foundFilesMatching = attachments.filter((attachment) => {
return attachment.extendedData.info.extension === existingFile.extendedData.info.extension
&& attachment.extendedData.info.filename.startsWith(fileName);
});
let nextIndex = foundFilesMatching.length + 1;
fileItem.file.name = fileName + ' (' + nextIndex + ').' + existingFile.extendedData.info.extension;
self.runUpload(fileItem);
}
}
);
} else {
self.runUpload(fileItem);
}
}, function (error) {
});
}
onSuccessItem (item, response) {
let attachments = this.cardservice.get(item.cardId).attachments;
let index = attachments.indexOf(attachments.find((attachment) => attachment.id === response.id));
if (~index) {
attachments = attachments.splice(index, 1);
}
this.cardservice.get(item.cardId).attachments.push(response);
}
}
app.service('FileService', FileService);

View File

@@ -21,7 +21,8 @@
*/
import app from '../app/App.js';
app.factory('StackService', function (ApiService, $http, $q) {
/* global app angular */
app.factory('StackService', function (ApiService, CardService, $http, $q) {
var StackService = function ($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
@@ -32,6 +33,12 @@ app.factory('StackService', function (ApiService, $http, $q) {
$http.get(this.baseUrl + '/' + boardId).then(function (response) {
self.clear();
self.addAll(response.data);
// When loading a stack add cards to the CardService so we can fetch
// information from there. That way we don't need to refresh the whole
// stack data during digest if some value changes
angular.forEach(response.data, function (entity) {
CardService.addAll(entity.cards);
});
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Error while loading stacks');
@@ -45,6 +52,9 @@ app.factory('StackService', function (ApiService, $http, $q) {
$http.get(this.baseUrl + '/' + boardId + '/archived').then(function (response) {
self.clear();
self.addAll(response.data);
angular.forEach(response.data, function (entity) {
CardService.addAll(entity.cards);
});
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Error while loading stacks');
@@ -119,7 +129,7 @@ app.factory('StackService', function (ApiService, $http, $q) {
}
};
// FIXME: Should not sure popup but proper undo mechanism
// FIXME: Should not show popup but proper undo mechanism
StackService.prototype.delete = function (id) {
var deferred = $q.defer();
var self = this;

View File

@@ -0,0 +1,75 @@
<?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\Controller;
use OCA\Deck\Service\AttachmentService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class AttachmentController extends Controller {
/** @var AttachmentService */
private $attachmentService;
public function __construct($appName, IRequest $request, AttachmentService $attachmentService) {
parent::__construct($appName, $request);
$this->attachmentService = $attachmentService;
}
public function getAll($cardId) {
return $this->attachmentService->findAll($cardId);
}
/**
* @param $cardId
* @param $attachmentId
* @NoCSRFRequired
* @return \OCP\AppFramework\Http\Response
* @throws \OCA\Deck\NotFoundException
*/
public function display($cardId, $attachmentId) {
return $this->attachmentService->display($cardId, $attachmentId);
}
public function create($cardId) {
return $this->attachmentService->create(
$cardId,
$this->request->getParam('type'),
$this->request->getParam('data')
);
}
public function update($cardId, $attachmentId) {
return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data'));
}
public function delete($cardId, $attachmentId) {
return $this->attachmentService->delete($cardId, $attachmentId);
}
public function restore($cardId, $attachmentId) {
return $this->attachmentService->restore($cardId, $attachmentId);
}
}

View File

@@ -26,12 +26,13 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Db\Acl;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\ApiController;
use OCP\IRequest;
use OCP\AppFramework\Controller;
use OCP\IUserManager;
use OCP\IGroupManager;
class BoardController extends Controller {
class BoardController extends ApiController {
private $userId;
private $boardService;
private $userManager;

View File

@@ -21,25 +21,28 @@
*
*/
/**
* Created by PhpStorm.
* User: jus
* Date: 16.05.17
* Time: 12:34
*/
namespace OCA\Deck\Cron;
use OC\BackgroundJob\Job;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\Service\AttachmentService;
class DeleteCron extends Job {
/** @var BoardMapper */
private $boardMapper;
/** @var AttachmentService */
private $attachmentService;
/** @var AttachmentMapper */
private $attachmentMapper;
public function __construct(BoardMapper $boardMapper) {
public function __construct(BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
$this->boardMapper = $boardMapper;
$this->attachmentService = $attachmentService;
$this->attachmentMapper = $attachmentMapper;
}
/**
@@ -51,6 +54,18 @@ class DeleteCron extends Job {
foreach ($boards as $board) {
$this->boardMapper->delete($board);
}
$attachments = $this->attachmentMapper->findToDelete();
foreach ($attachments as $attachment) {
try {
$service = $this->attachmentService->getService($attachment->getType());
$service->delete($attachment);
} catch (InvalidAttachmentType $e) {
// Just delete the attachment if no service is available
}
$this->attachmentMapper->delete($attachment);
}
}
}

47
lib/Db/Attachment.php Normal file
View File

@@ -0,0 +1,47 @@
<?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\Db;
class Attachment extends RelationalEntity {
protected $cardId;
protected $type;
protected $data;
protected $lastModified = 0;
protected $createdAt = 0;
protected $createdBy;
protected $deletedAt = 0;
protected $extendedData = [];
public function __construct() {
$this->addType('id', 'integer');
$this->addType('cardId', 'integer');
$this->addType('lastModified', 'integer');
$this->addType('createdAt', 'integer');
$this->addType('deletedAt', 'integer');
$this->addResolvable('createdBy');
$this->addRelation('extendedData');
}
}

147
lib/Db/AttachmentMapper.php Normal file
View File

@@ -0,0 +1,147 @@
<?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\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUserManager;
use PDO;
class AttachmentMapper extends DeckMapper implements IPermissionMapper {
private $cardMapper;
private $userManager;
private $qb;
public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager) {
parent::__construct($db, 'deck_attachment', Attachment::class);
$this->cardMapper = $cardMapper;
$this->userManager = $userManager;
$this->qb = $this->db->getQueryBuilder();
}
/**
* @param $id
* @return Entity|Attachment
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function find($id) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_attachment')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$cursor = $qb->execute();
$row = $cursor->fetch(PDO::FETCH_ASSOC);
$cursor->closeCursor();
return $this->mapRowToEntity($row);
}
/**
* Find all attachments for a card
*
* @param $cardId
* @return array
*/
public function findAll($cardId) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_attachment')
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$entities = [];
$cursor = $qb->execute();
while($row = $cursor->fetch()){
$entities[] = $this->mapRowToEntity($row);
}
$cursor->closeCursor();
return $entities;
}
public function findToDelete($cardId = null, $withOffset = true) {
// add buffer of 5 min
$timeLimit = time() - (60 * 5);
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_attachment')
->where($qb->expr()->gt('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
if ($withOffset) {
$qb
->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($timeLimit, IQueryBuilder::PARAM_INT)));
}
if ($cardId !== null) {
$qb
->andWhere($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
}
$entities = [];
$cursor = $qb->execute();
while($row = $cursor->fetch()){
$entities[] = $this->mapRowToEntity($row);
}
$cursor->closeCursor();
return $entities;
}
/**
* Check if $userId is owner of Entity with $id
*
* @param $userId string userId
* @param $id int|string unique entity identifier
* @return boolean
*/
public function isOwner($userId, $id) {
try {
$attachment = $this->find($id);
return $this->cardMapper->isOwner($userId, $attachment->getCardId());
} catch (DoesNotExistException $e) {
} catch (MultipleObjectsReturnedException $e) {
}
return false;
}
/**
* Query boardId for Entity of given $id
*
* @param $id int|string unique entity identifier
* @return int|null id of Board
*/
public function findBoardId($id) {
try {
$attachment = $this->find($id);
} catch (\Exception $e) {
return null;
}
return $this->cardMapper->findBoardId($attachment->getCardId());
}
}

View File

@@ -35,6 +35,8 @@ class Card extends RelationalEntity {
protected $createdAt;
protected $labels;
protected $assignedUsers;
protected $attachments;
protected $attachmentCount;
protected $owner;
protected $order;
protected $archived = false;
@@ -58,6 +60,8 @@ class Card extends RelationalEntity {
$this->addType('notified', 'boolean');
$this->addRelation('labels');
$this->addRelation('assignedUsers');
$this->addRelation('attachments');
$this->addRelation('attachmentCount');
$this->addRelation('participants');
$this->addResolvable('owner');
}

View File

@@ -25,6 +25,13 @@ namespace OCA\Deck\Db;
use OCP\AppFramework\Db\Mapper;
/**
* Class DeckMapper
*
* @package OCA\Deck\Db
*
* TODO: Move to QBMapper once Nextcloud 14 is a minimum requirement
*/
abstract class DeckMapper extends Mapper {
/**

View File

@@ -0,0 +1,37 @@
<?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;
class InvalidAttachmentType extends \Exception {
/**
* InvalidAttachmentType constructor.
*/
public function __construct($type) {
parent::__construct('No matching IAttachmentService implementation found for type ' . $type);
}
}

View File

@@ -0,0 +1,264 @@
<?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\Service;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCP\AppFramework\Http\Response;
use OCP\ICache;
use OCP\ICacheFactory;
class AttachmentService {
private $attachmentMapper;
private $cardMapper;
private $permissionService;
private $userId;
/** @var IAttachmentService[] */
private $services = [];
private $application;
/** @var ICache */
private $cache;
/**
* AttachmentService constructor.
*
* @param AttachmentMapper $attachmentMapper
* @param CardMapper $cardMapper
* @param PermissionService $permissionService
* @param Application $application
* @param ICacheFactory $cacheFactory
* @param $userId
* @throws \OCP\AppFramework\QueryException
*/
public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId) {
$this->attachmentMapper = $attachmentMapper;
$this->cardMapper = $cardMapper;
$this->permissionService = $permissionService;
$this->userId = $userId;
$this->application = $application;
$this->cache = $cacheFactory->createDistributed('deck-card-attachments-');
// Register shipped attachment services
// TODO: move this to a plugin based approach once we have different types of attachments
$this->registerAttachmentService('deck_file', FileService::class);
}
/**
* @param string $type
* @param string $class
* @throws \OCP\AppFramework\QueryException
*/
public function registerAttachmentService($type, $class) {
$this->services[$type] = $this->application->getContainer()->query($class);
}
/**
* @param string $type
* @return IAttachmentService
* @throws InvalidAttachmentType
*/
public function getService($type) {
if (isset($this->services[$type])) {
return $this->services[$type];
}
throw new InvalidAttachmentType($type);
}
/**
* @param $cardId
* @return array
* @throws \OCA\Deck\NoPermissionException
*/
public function findAll($cardId, $withDeleted = false) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$attachments = $this->attachmentMapper->findAll($cardId);
if ($withDeleted) {
$attachments = array_merge($attachments, $this->attachmentMapper->findToDelete($cardId, false));
}
foreach ($attachments as &$attachment) {
try {
$service = $this->getService($attachment->getType());
$service->extendData($attachment);
} catch (InvalidAttachmentType $e) {
// Ingore invalid attachment types when extending the data
}
}
return $attachments;
}
public function count($cardId) {
$count = $this->cache->get('card-' . $cardId);
if (!$count) {
$count = count($this->attachmentMapper->findAll($cardId));
$this->cache->set('card-' . $cardId, $count);
}
return $count;
}
public function create($cardId, $type, $data) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $cardId);
$attachment = new Attachment();
$attachment->setCardId($cardId);
$attachment->setType($type);
$attachment->setData($data);
$attachment->setCreatedBy($this->userId);
$attachment->setLastModified(time());
$attachment->setCreatedAt(time());
try {
$service = $this->getService($attachment->getType());
$service->create($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
$attachment = $this->attachmentMapper->insert($attachment);
// extend data so the frontend can use it properly after creating
try {
$service = $this->getService($attachment->getType());
$service->extendData($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
return $attachment;
}
/**
* Display the attachment
*
* @param $cardId
* @param $attachmentId
* @return Response
* @throws NotFoundException
*/
public function display($cardId, $attachmentId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$attachment = $this->attachmentMapper->find($attachmentId);
try {
$service = $this->getService($attachment->getType());
return $service->display($attachment);
} catch (InvalidAttachmentType $e) {
throw new NotFoundException();
}
}
/**
* Update an attachment with custom data
*
* @param $cardId
* @param $attachmentId
* @param $request
* @return mixed
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function update($cardId, $attachmentId, $data) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $cardId);
$attachment = $this->attachmentMapper->find($attachmentId);
$attachment->setData($data);
try {
$service = $this->getService($attachment->getType());
$service->update($attachment);
} catch (InvalidAttachmentType $e) {
// just update without further action
}
$attachment->setLastModified(time());
$this->attachmentMapper->update($attachment);
// extend data so the frontend can use it properly after creating
try {
$service = $this->getService($attachment->getType());
$service->extendData($attachment);
} catch (InvalidAttachmentType $e) {
// just store the data
}
return $attachment;
}
/**
* Either mark an attachment as deleted for later removal or just remove it depending
* on the IAttachmentService implementation
*
* @param $cardId
* @param $attachmentId
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function delete($cardId, $attachmentId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $cardId);
$attachment = $this->attachmentMapper->find($attachmentId);
try {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment);
} catch (InvalidAttachmentType $e) {
// just delete without further action
}
return $this->attachmentMapper->delete($attachment);
}
public function restore($cardId, $attachmentId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $cardId);
$attachment = $this->attachmentMapper->find($attachmentId);
try {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$attachment->setDeletedAt(0);
return $this->attachmentMapper->update($attachment);
}
} catch (InvalidAttachmentType $e) {
}
throw new NoPermissionException('Restore is not allowed.');
}
}

View File

@@ -40,20 +40,24 @@ class CardService {
private $permissionService;
private $boardService;
private $assignedUsersMapper;
private $attachmentService;
public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService, AssignedUsersMapper $assignedUsersMapper) {
public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService, AssignedUsersMapper $assignedUsersMapper, AttachmentService $attachmentService) {
$this->cardMapper = $cardMapper;
$this->stackMapper = $stackMapper;
$this->permissionService = $permissionService;
$this->boardService = $boardService;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
}
public function find($cardId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
$card = $this->cardMapper->find($cardId);
$assignedUsers = $this->assignedUsersMapper->find($card->getId());
$attachments = $this->attachmentService->findAll($cardId, true);
$card->setAssignedUsers($assignedUsers);
$card->setAttachments($attachments);
return $card;
}

196
lib/Service/FileService.php Normal file
View File

@@ -0,0 +1,196 @@
<?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\Service;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OCA\Deck\Db\Attachment;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
class FileService implements IAttachmentService {
private $l10n;
private $appData;
private $request;
private $logger;
public function __construct(
IL10N $l10n,
IAppData $appData,
IRequest $request,
ILogger $logger
) {
$this->l10n = $l10n;
$this->appData = $appData;
$this->request = $request;
$this->logger = $logger;
}
/**
* @param Attachment $attachment
* @return ISimpleFile
* @throws NotFoundException
* @throws NotPermittedException
*/
private function getFileForAttachment(Attachment $attachment) {
return $this->getFolder($attachment)
->getFile($attachment->getData());
}
/**
* @param Attachment $attachment
* @return ISimpleFolder
* @throws NotPermittedException
*/
private function getFolder(Attachment $attachment) {
$folderName = 'file-card-' . (int)$attachment->getCardId();
try {
$folder = $this->appData->getFolder($folderName);
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder($folderName);
}
return $folder;
}
public function extendData(Attachment $attachment) {
try {
$file = $this->getFileForAttachment($attachment);
} catch (NotFoundException $e) {
$this->logger->info('Extending data for file attachment failed');
return $attachment;
} catch (NotPermittedException $e) {
$this->logger->info('Extending data for file attachment failed');
return $attachment;
}
$attachment->setExtendedData([
'filesize' => $file->getSize(),
'mimetype' => $file->getMimeType(),
'info' => pathinfo($file->getName())
]);
return $attachment;
}
private function getUploadedFile () {
$file = $this->request->getUploadedFile('file');
$error = null;
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];
if (empty($file)) {
$error = $this->l10n->t('No file uploaded');
}
if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
$error = $phpFileUploadErrors[$file['error']];
}
if ($error !== null) {
throw new \Exception($error);
}
return $file;
}
public function create(Attachment $attachment) {
$file = $this->getUploadedFile();
$folder = $this->getFolder($attachment);
$fileName = $file['name'];
if ($folder->fileExists($fileName)) {
throw new \Exception('File already exists.');
}
$target = $folder->newFile($fileName);
$target->putContent(file_get_contents($file['tmp_name'], 'r'));
$attachment->setData($fileName);
}
/**
* This method requires to be used with POST so we can properly get the form data
*/
public function update(Attachment $attachment) {
$file = $this->getUploadedFile();
$fileName = $file['name'];
$attachment->setData($fileName);
$target = $this->getFileForAttachment($attachment);
$target->putContent(file_get_contents($file['tmp_name'], 'r'));
$attachment->setLastModified(time());
}
public function delete(Attachment $attachment) {
try {
$file = $this->getFileForAttachment($attachment);
$file->delete();
} catch (NotFoundException $e) {
}
}
public function display(Attachment $attachment) {
$file = $this->getFileForAttachment($attachment);
$response = new FileDisplayResponse($file);
if ($file->getMimeType() === 'application/pdf') {
// We need those since otherwise chrome won't show the PDF file with CSP rule object-src 'none'
// https://bugs.chromium.org/p/chromium/issues/detail?id=271452
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$response->setContentSecurityPolicy($policy);
}
$response->addHeader('Content-Type', $file->getMimeType());
return $response;
}
/**
* Should undo be allowed and the delete action be done by a background job
*
* @return bool
*/
public function allowUndo() {
return true;
}
/**
* Mark an attachment as deleted
*
* @param Attachment $attachment
*/
public function markAsDeleted(Attachment $attachment) {
$attachment->setDeletedAt(time());
}
}

View File

@@ -0,0 +1,99 @@
<?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\Service;
use OCA\Deck\Db\Attachment;
use OCP\AppFramework\Http\Response;
/**
* Interface IAttachmentService
*
* Implement this interface to extend the default attachment behaviour
* This interface allows to extend/reduce the data stored with an attachment,
* as well as rendering a custom output per attachment type
*
*/
interface IAttachmentService {
/**
* Add extended data to the returned data of an attachment
*
* @param Attachment $attachment
* @return mixed
*/
public function extendData(Attachment $attachment);
/**
* Display the attachment
*
* TODO: Move to IAttachmentDisplayService for better separation
*
* @param Attachment $attachment
* @return Response
*/
public function display(Attachment $attachment);
/**
* Create a new attachment
*
* This method will be called before inserting the attachment entry in the database
*
* @param Attachment $attachment
*/
public function create(Attachment $attachment);
/**
* Update an attachment with custom data
*
* This method will be called before updating the attachment entry in the database
*
* @param Attachment $attachment
*/
public function update(Attachment $attachment);
/**
* Delete an attachment
*
* This method will be called before removing the attachment entry from the database
*
* @param Attachment $attachment
*/
public function delete(Attachment $attachment);
/**
* Should undo be allowed and the delete action be done by a background job
*
* @return bool
*/
public function allowUndo();
/**
* Mark an attachment as deleted
*
* @param Attachment $attachment
*/
public function markAsDeleted(Attachment $attachment);
}

View File

@@ -30,6 +30,8 @@ use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Stack;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\StatusException;
use OCP\ICache;
use OCP\ICacheFactory;
class StackService {
@@ -40,6 +42,7 @@ class StackService {
private $permissionService;
private $boardService;
private $assignedUsersMapper;
private $attachmentService;
public function __construct(
StackMapper $stackMapper,
@@ -47,7 +50,8 @@ class StackService {
LabelMapper $labelMapper,
PermissionService $permissionService,
BoardService $boardService,
AssignedUsersMapper $assignedUsersMapper
AssignedUsersMapper $assignedUsersMapper,
AttachmentService $attachmentService
) {
$this->stackMapper = $stackMapper;
$this->cardMapper = $cardMapper;
@@ -55,6 +59,7 @@ class StackService {
$this->permissionService = $permissionService;
$this->boardService = $boardService;
$this->assignedUsersMapper = $assignedUsersMapper;
$this->attachmentService = $attachmentService;
}
public function findAll($boardId) {
@@ -69,6 +74,7 @@ class StackService {
if (array_key_exists($card->id, $labels)) {
$cards[$cardIndex]->setLabels($labels[$card->id]);
}
$card->setAttachmentCount($this->attachmentService->count($card->getId()));
}
$stacks[$stackIndex]->setCards($cards);
}

View File

@@ -58,5 +58,8 @@ Util::addScript('deck', 'build/deck');
<script type="text/ng-template" id="/card.sidebarView.html">
<?php print_unescaped($this->inc('part.card')); ?>
</script>
<script type="text/ng-template" id="/card.attachments.html">
<?php print_unescaped($this->inc('part.card.attachments')); ?>
</script>
</div>

View File

@@ -61,12 +61,16 @@
data-as-sortable-item
ng-click="$event.stopPropagation()"
ui-sref="board.card({boardId: id, cardId: c.id})"
ng-class="{'archived': c.archived, 'has-labels': c.labels.length>0, 'current': c.id == params.cardId }">
ng-class="{'archived': cardservice.get(c.id).archived, 'has-labels': cardservice.get(c.id).labels.length>0, 'current': cardservice.get(c.id).id == params.cardId }"
nv-file-drop="" uploader="uploader" options="{cardId: c.id}">
<div class="drop-indicator" uploader="uploader" nv-file-over>
<p><?php p($l->t('Drop your files here to upload it to the card')); ?></p>
</div>
<div data-as-sortable-item-handle>
<div class="card-upper">
<h4>{{ c.title }}</h4>
<h4>{{ cardservice.get(c.id).title }}</h4>
<ul class="labels">
<li ng-repeat="label in c.labels"
<li ng-repeat="label in cardservice.get(c.id).labels"
ng-style="labelStyle(label.color)" title="{{ label.title }}">
<span>{{ label.title }}</span>
</li>
@@ -75,17 +79,21 @@
</div>
<div class="card-controls">
<i class="icon icon-filetype-text" ng-if="c.description" title="{{ c.description }}"></i>
<span class="due" ng-if="c.duedate" ng-class="{'overdue': c.overdue == 3, 'now': c.overdue == 2, 'next': c.overdue == 1 }" title="{{ c.duedate }}">
<i class="icon icon-filetype-text" ng-if="cardservice.get(c.id).description" title="{{ cardservice.get(c.id).description }}"></i>
<span class="due" ng-if="cardservice.get(c.id).duedate" ng-class="{'overdue': cardservice.get(c.id).overdue == 3, 'now': cardservice.get(c.id).overdue == 2, 'next': cardservice.get(c.id).overdue == 1 }" title="{{ cardservice.get(c.id).duedate }}">
<i class="icon icon-badge"></i>
<span data-timestamp="{{ c.duedate | dateToTimestamp }}" class="live-relative-timestamp">{{ c.duedate | relativeDateFilterString }}</span>
<span data-timestamp="{{ cardservice.get(c.id).duedate | dateToTimestamp }}" class="live-relative-timestamp">{{ cardservice.get(c.id).duedate | relativeDateFilterString }}</span>
</span>
<div class="card-tasks" ng-if="getCheckboxes(c.description)[1] > 0">
<div class="card-tasks" ng-if="getCheckboxes(cardservice.get(c.id).description)[1] > 0">
<i class="icon icon-checkmark"></i>
<span>{{ getCheckboxes(c.description)[0] }}/{{ getCheckboxes(c.description)[1] }}</span>
<span>{{ getCheckboxes(cardservice.get(c.id).description)[0] }}/{{ getCheckboxes(cardservice.get(c.id).description)[1] }}</span>
</div>
<div class="card-files" ng-if="attachmentCount(cardservice.get(c.id)) > 0">
<i class="icon icon-files-dark"></i>
<span>{{ attachmentCount(cardservice.get(c.id)) }}</span>
</div>
<div class="card-assigned-users">
<div class="assigned-user" ng-repeat="user in c.assignedUsers | limitTo: 3">
<div class="assigned-user" ng-repeat="user in cardservice.get(c.id).assignedUsers | limitTo: 3">
<avatar data-user="{{ user.participant.uid }}" data-displayname="{{ user.participant.displayname }}" data-tooltip></avatar>
</div>
</div>

View File

@@ -45,7 +45,7 @@
<td>
<div id="assigned-users">
<avatar data-contactsmenu data-tooltip data-user="{{ b.owner.uid }}" data-displayname="{{ b.owner.displayname }}"></avatar>
<avatar data-contactsmenu data-tooltip data-user="{{ acl.participant.uid }}" data-displayname="{{ acl.participant.displayname }}" ng-repeat="acl in b.acl | limitTo: 7 track by acl.i"></avatar>
<avatar data-contactsmenu data-tooltip data-user="{{ acl.participant.uid }}" data-displayname="{{ acl.participant.displayname }}" ng-repeat="acl in b.acl | limitTo: 7 track by acl.id"></avatar>
</div>
</td>
<td>

View File

@@ -0,0 +1,42 @@
<div ng-class="{'attachment-list-wrapper': $ctrl.isFileSelector}">
<div class="attachment-list" ng-class="{selector: $ctrl.isFileSelector}">
<h3 class="attachment-selector" ng-if="$ctrl.isFileSelector"><?php p($l->t('Select an attachment')); ?> <a class="icon-close" ng-click="$ctrl.abort()"></a></h3>
<ul>
<li class="attachment"
ng-repeat="attachment in $ctrl.cardservice.getCurrent().attachments | filter: {type: 'deck_file'} | orderBy: ['deletedAt', '-lastModified']"
ng-class="{deleted: attachment.deletedAt > 0, selector: $ctrl.isFileSelector}"
ng-if="!$ctrl.isFileSelector || attachment.deletedAt == 0">
<a class="fileicon" ng-style="$ctrl.mimetypeForAttachment(attachment)" ng-href="{{ attachmentUrl(attachment) }}"></a>
<div class="details">
<a ng-href="{{ $ctrl.attachmentUrl(attachment) }}" target="_blank">
<div class="filename">
<span class="basename">{{ attachment.extendedData.info.filename}}</span>
<span class="extension">.{{ attachment.extendedData.info.extension}}</span>
</div>
<span class="filesize">{{ attachment.extendedData.filesize | bytes }}</span>
<span class="filedate">{{ attachment.lastModified|relativeDateFilter }}</span>
<span class="filedate"><?php p($l->t('by')); ?> {{ attachment.createdBy }}</span>
</a>
</div>
<button class="icon icon-history button-inline" ng-click="$ctrl.cardservice.attachmentRemoveUndo(attachment)" ng-if="!$ctrl.isFileSelector && attachment.deletedAt > 0" title="<?php p($l->t('Undo file deletion - Otherwise the file will be deleted during the next cronjob run.')); ?>">
<span class="hidden-visually"><?php p($l->t('Undo file deletion')); ?></span>
</button>
<button class="icon icon-confirm button-inline" ng-click="$ctrl.select(attachment)" ng-if="$ctrl.isFileSelector">
<span class="hidden-visually"><?php p($l->t('Insert the file into the description')); ?></span>
</button>
<div class="app-popover-menu-utils" ng-if="!$ctrl.isFileSelector && attachment.deletedAt == 0">
<button class="button-inline icon icon-more"></button>
<div class="popovermenu hidden">
<ul>
<li>
<a class="menuitem action action-delete"
ng-click="$ctrl.cardservice.attachmentRemove(attachment); $event.stopPropagation();"><span
class="icon icon-delete"></span><span><?php p($l->t('Delete')); ?></span></a>
</li>
</ul>
</div>
</div>
</a>
</li>
</ul>
</div>

View File

@@ -1,4 +1,8 @@
<div id="board-status" ng-if="statusservice.active">
<div nv-file-drop="" uploader="uploader" class="drop-zone" options="{cardId: cardservice.getCurrent().id}">
<div class="drop-indicator" nv-file-over uploader="uploader">
<p><?php p($l->t('Drop your files here to upload it to the card')); ?></p>
</div>
<div id="board-status" ng-if="statusservice.active">
<div id="emptycontent">
<div class="icon-{{ statusservice.icon }}" title="<?php p($l->t('Status')); ?>"><span class="hidden-visually"><?php p($l->t('Status')); ?></span></div>
<h2>{{ statusservice.title }}</h2>
@@ -83,20 +87,36 @@
<button class="icon icon-delete button-inline" title="<?php p($l->t('Remove due date')); ?>" ng-if="cardservice.getCurrent().duedate" ng-click="resetDuedate()"><span class="hidden-visually"><?php p($l->t('Remove due date')); ?></span></button>
</div>
<div class="section-header card-description">
<h4>
<div>
<?php p($l->t('Description')); ?>
<a href="https://github.com/nextcloud/deck/wiki/Markdown-Help" target="_blank" class="icon icon-help" data-toggle="tooltip" data-placement="right" title="<?php p($l->t('Formatting help')); ?>"><span class="hidden-visually"><?php p($l->t('Formatting help')); ?></span></a>
</div>
</h4>
<span class="save-indicator saved"><?php p($l->t('Saved')); ?></span>
<span class="save-indicator unsaved"><?php p($l->t('Unsaved changes')); ?></span>
<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==1)}" ui-sref="{tab: 1}"><a><?php p($l->t('Attachments')); ?></a></li>
</ul>
<div class="tabDetails">
<span class="save-indicator saved"><?php p($l->t('Saved')); ?></span>
<span class="save-indicator unsaved"><?php p($l->t('Unsaved changes')); ?></span>
<a ng-if="params.tab === 0" href="https://github.com/nextcloud/deck/wiki/Markdown-Help" target="_blank" class="icon icon-help" data-toggle="tooltip" data-placement="left" title="<?php p($l->t('Formatting help')); ?>"><span class="hidden-visually"><?php p($l->t('Formatting help')); ?></span></a>
<label ng-if="params.tab === 1" for="attachment-upload" class="button icon-upload" ng-class="{'icon-loading-small': uploader.isUploading}" data-toggle="tooltip" data-placement="left" title="<?php p($l->t('Upload attachment')); ?>"></label>
<input id="attachment-upload" type="file" nv-file-select="" uploader="uploader" class="hidden" options="{cardId: cardservice.getCurrent().id}"/>
<input ng-if="status.cardEditDescription" type="button" ng-mousedown="status.continueEdit = true; status.selectAttachment = true;" class="icon-files-dark" data-toggle="tooltip" data-placement="left" title="<?php p($l->t('Insert attachment')); ?>"/>
</div>
</div>
<div class="section-content card-description">
<div class="section-content card-attachments">
<attachment-list-component ng-if="params.tab === 1 && cardservice.getCurrent() && isArray(cardservice.getCurrent().attachments)" attachments="cardservice.getCurrent().attachments"></attachment-list-component>
</div>
<div class="section-content card-description" ng-if="params.tab === 0">
<attachment-list-component
ng-if="status.selectAttachment"
attachments="cardservice.getCurrent().attachments"
is-file-selector="true"
on-select="addAttachmentToDescription(attachment)" on-abort="abortAttachmentSelection()">
</attachment-list-component>
<textarea elastic ng-if="status.cardEditDescription"
placeholder="<?php p($l->t('Add a card description…')); ?>"
ng-blur="cardUpdate(status.edit)"
ng-blur="!status.continueEdit && cardUpdate(status.edit)"
ng-model="status.edit.description"
ng-change="cardEditDescriptionChanged(); updateMarkdown(status.edit.description)"
autofocus-on-insert> </textarea>

View File

@@ -23,20 +23,31 @@
namespace OCA\Deck\Cron;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Service\IAttachmentService;
class DeleteCronTest extends \Test\TestCase {
/** @var BoardMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var BoardMapper|\PHPUnit\Framework\MockObject\MockObject */
protected $boardMapper;
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var AttachmentMapper|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentMapper;
/** @var DeleteCron */
protected $deleteCron;
public function setUp() {
parent::setUp();
$this->boardMapper = $this->createMock(BoardMapper::class);
$this->deleteCron = new DeleteCron($this->boardMapper);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->deleteCron = new DeleteCron($this->boardMapper, $this->attachmentService, $this->attachmentMapper);
}
protected function getBoard($id) {
@@ -67,6 +78,44 @@ class DeleteCronTest extends \Test\TestCase {
$this->boardMapper->expects($this->at(4))
->method('delete')
->with($boards[3]);
$attachment = new Attachment();
$attachment->setType('deck_file');
$this->attachmentMapper->expects($this->once())
->method('findToDelete')
->willReturn([
$attachment
]);
$service = $this->createMock(IAttachmentService::class);
$service->expects($this->once())
->method('delete')
->with($attachment);
$this->attachmentService->expects($this->once())
->method('getService')
->willReturn($service);
$this->attachmentMapper->expects($this->once())
->method('delete')
->with($attachment);
$this->invokePrivate($this->deleteCron, 'run', [null]);
}
public function testDeleteCronInvalidAttachment() {
$boards = [];
$this->boardMapper->expects($this->once())
->method('findToDelete')
->willReturn($boards);
$attachment = new Attachment();
$attachment->setType('deck_file_invalid');
$this->attachmentMapper->expects($this->once())
->method('findToDelete')
->willReturn([$attachment]);
$this->attachmentService->expects($this->once())
->method('getService')
->will($this->throwException(new InvalidAttachmentType('deck_file_invalid')));
$this->attachmentMapper->expects($this->once())
->method('delete')
->with($attachment);
$this->invokePrivate($this->deleteCron, 'run', [null]);
}
}

View File

@@ -31,9 +31,9 @@ use OCA\Deck\Notification\NotificationHelper;
class ScheduledNoificationsTest extends \Test\TestCase {
/** @var CardMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var CardMapper|\PHPUnit\Framework\MockObject\MockObject */
protected $cardMapper;
/** @var NotificationHelper|\PHPUnit_Framework_MockObject_MockObject */
/** @var NotificationHelper|\PHPUnit\Framework\MockObject\MockObject */
protected $notificationHelper;
/** @var ScheduledNotifications */
protected $scheduledNotifications;

View File

@@ -0,0 +1,148 @@
<?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\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use Test\AppFramework\Db\MapperTestUtility;
/**
* @group DB
*/
class AttachmentMapperTest extends MapperTestUtility {
/** @var IDBConnection */
private $dbConnection;
/** @var AttachmentMapper */
private $attachmentMapper;
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
private $userManager;
/** @var CardMapper|\PHPUnit\Framework\MockObject\MockObject */
private $cardMapper;
// Data
private $attachments;
private $attachmentsById = [];
public function setup(){
parent::setUp();
$this->userManager = $this->createMock(IUserManager::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->dbConnection = \OC::$server->getDatabaseConnection();
$this->attachmentMapper = new AttachmentMapper(
$this->dbConnection,
$this->cardMapper,
$this->userManager
);
$this->attachments = [
$this->createAttachmentEntity(1, 'deck_file', 'file1.pdf'),
$this->createAttachmentEntity(1, 'deck_file', 'file2.pdf'),
$this->createAttachmentEntity(2, 'deck_file', 'file3.pdf'),
$this->createAttachmentEntity(3, 'deck_file', 'file4.pdf')
];
foreach ($this->attachments as $attachment) {
$entry = $this->attachmentMapper->insert($attachment);
$entry->resetUpdatedFields();
$this->attachmentsById[$entry->getId()] = $entry;
}
}
private function createAttachmentEntity($cardId, $type, $data) {
$attachment = new Attachment();
$attachment->setCardId($cardId);
$attachment->setType($type);
$attachment->setData($data);
$attachment->setCreatedBy('admin');
return $attachment;
}
public function testFind() {
foreach ($this->attachmentsById as $id => $attachment) {
$this->assertEquals($attachment, $this->attachmentMapper->find($id));
}
}
public function testFindAll() {
$attachmentsByCard = [
$this->attachmentMapper->findAll(1),
$this->attachmentMapper->findAll(2),
$this->attachmentMapper->findAll(3)
];
$this->assertEquals($attachmentsByCard[0], $this->attachmentMapper->findAll(1));
$this->assertEquals($attachmentsByCard[1], $this->attachmentMapper->findAll(2));
$this->assertEquals($attachmentsByCard[2], $this->attachmentMapper->findAll(3));
$this->assertEquals([], $this->attachmentMapper->findAll(5));
}
public function testFindToDelete() {
$attachmentsToDelete = $this->attachments;
$attachmentsToDelete[0]->setDeletedAt(1);
$attachmentsToDelete[2]->setDeletedAt(1);
$this->attachmentMapper->update($attachmentsToDelete[0]);
$this->attachmentMapper->update($attachmentsToDelete[2]);
foreach ($attachmentsToDelete as $attachment) {
$attachment->resetUpdatedFields();
}
$this->assertEquals([$attachmentsToDelete[0]], $this->attachmentMapper->findToDelete(1));
$this->assertEquals([$attachmentsToDelete[2]], $this->attachmentMapper->findToDelete(2));
}
public function testIsOwner() {
$this->cardMapper->expects($this->once())
->method('isOwner')
->with('admin', 1)
->willReturn(true);
$this->assertTrue($this->attachmentMapper->isOwner('admin', (string)$this->attachments[0]->getId()));
}
public function testIsOwnerInvalid() {
$this->cardMapper->expects($this->once())
->method('isOwner')
->with('admin', 1)
->will($this->throwException(new DoesNotExistException('does not exist')));
$this->assertFalse($this->attachmentMapper->isOwner('admin', (string)$this->attachments[0]->getId()));
}
public function testFindBoardId() {
$this->cardMapper->expects($this->any())
->method('findBoardId')
->willReturn(123);
foreach ($this->attachmentsById as $attachment) {
$this->assertEquals(123, $this->attachmentMapper->findBoardId($attachment->getId()));
}
}
public function tearDown() {
parent::tearDown();
foreach ($this->attachments as $attachment) {
$this->attachmentMapper->delete($attachment);
}
}
}

View File

@@ -0,0 +1,51 @@
<?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\Db;
class AttachmentTest extends \Test\TestCase {
private function createAttachment() {
$attachment = new Attachment();
$attachment->setId(1);
$attachment->setCardId(123);
$attachment->setData("blob");
$attachment->setCreatedBy('admin');
$attachment->setType('deck_file');
return $attachment;
}
public function testJsonSerialize() {
$board = $this->createAttachment();
$this->assertEquals([
'id' => 1,
'cardId' => 123,
'lastModified' => 0,
'data' => 'blob',
'type' => 'deck_file',
'createdAt' => 0,
'createdBy' => 'admin',
'deletedAt' => 0,
'extendedData' => []
], $board->jsonSerialize());
}
}

View File

@@ -35,13 +35,13 @@ class BoardMapperTest extends MapperTestUtility {
/** @var IDBConnection */
private $dbConnection;
/** @var AclMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var AclMapper|\PHPUnit\Framework\MockObject\MockObject */
private $aclMapper;
/** @var BoardMapper */
private $boardMapper;
/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
/** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
private $userManager;
/** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */
/** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
private $groupManager;
// Data

View File

@@ -25,8 +25,9 @@ namespace OCA\Deck\Db;
use DateInterval;
use DateTime;
use Test\TestCase;
class CardTest extends \PHPUnit_Framework_TestCase {
class CardTest extends TestCase {
private function createCard() {
$card = new Card();
$card->setId(1);
@@ -77,6 +78,8 @@ class CardTest extends \PHPUnit_Framework_TestCase {
'duedate' => null,
'overdue' => 0,
'archived' => false,
'attachments' => null,
'attachmentCount' => null,
'assignedUsers' => null,
], $card->jsonSerialize());
}
@@ -97,6 +100,8 @@ class CardTest extends \PHPUnit_Framework_TestCase {
'duedate' => null,
'overdue' => 0,
'archived' => false,
'attachments' => null,
'attachmentCount' => null,
'assignedUsers' => null,
], $card->jsonSerialize());
}
@@ -127,6 +132,8 @@ class CardTest extends \PHPUnit_Framework_TestCase {
'duedate' => null,
'overdue' => 0,
'archived' => false,
'attachments' => null,
'attachmentCount' => null,
'assignedUsers' => ['user1'],
], $card->jsonSerialize());
}

View File

@@ -23,7 +23,9 @@
namespace OCA\Deck\Db;
class LabelTest extends \PHPUnit_Framework_TestCase {
use Test\TestCase;
class LabelTest extends TestCase {
private function createLabel() {
$label = new Label();
$label->setId(1);

View File

@@ -23,7 +23,7 @@
namespace OCA\Deck\Db;
class StackTest extends \PHPUnit_Framework_TestCase {
class StackTest extends \Test\TestCase {
private function createStack() {
$board = new Stack();
$board->setId(1);

View File

@@ -25,11 +25,12 @@ namespace OCA\Deck\Db;
use OCA\Deck\ArchivedItemException;
use OCA\Deck\Controller\PageController;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException;
class ExceptionsTest extends \PHPUnit_Framework_TestCase {
class ExceptionsTest extends \Test\TestCase {
public function testNoPermissionException() {
$c = new \stdClass();
@@ -49,6 +50,11 @@ class ExceptionsTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals('foo', $e->getMessage());
}
public function testInvalidAttachmentType() {
$e = new InvalidAttachmentType('foo');
$this->assertEquals('No matching IAttachmentService implementation found for type foo', $e->getMessage());
}
public function testStatusException() {
$e = new StatusException('foo');
$this->assertEquals('foo', $e->getMessage());

View File

@@ -0,0 +1,365 @@
<?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\Service;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\IAppContainer;
use OCP\ICache;
use OCP\ICacheFactory;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/** @internal Just for testing the service registration */
class MyAttachmentService {
public function extendData(Attachment $attachment) {}
public function display(Attachment $attachment) {}
public function create(Attachment $attachment) {}
public function update(Attachment $attachment) {}
public function delete(Attachment $attachment) {}
public function allowUndo() {}
public function markAsDeleted(Attachment $attachment) {}
}
class AttachmentServiceTest extends TestCase {
/** @var AttachmentMapper|MockObject */
private $attachmentMapper;
/** @var CardMapper|MockObject */
private $cardMapper;
/** @var PermissionService|MockObject */
private $permissionService;
private $userId = 'admin';
/** @var Application|MockObject */
private $application;
private $cacheFactory;
/** @var AttachmentService */
private $attachmentService;
/** @var MockObject */
private $attachmentServiceImpl;
private $appContainer;
/** ICache */
private $cache;
/**
* @throws \OCP\AppFramework\QueryException
*/
public function setUp() {
parent::setUp();
$this->attachmentServiceImpl = $this->createMock(IAttachmentService::class);
$this->appContainer = $this->createMock(IAppContainer::class);
$this->attachmentMapper = $this->createMock(AttachmentMapper::class);
$this->cardMapper = $this->createMock(CardMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
$this->application = $this->createMock(Application::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->cache = $this->createMock(ICache::class);
$this->cacheFactory->expects($this->any())->method('createDistributed')->willReturn($this->cache);
$this->appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($this->attachmentServiceImpl);
$this->application->expects($this->any())
->method('getContainer')
->willReturn($this->appContainer);
$this->attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $this->application, $this->cacheFactory, $this->userId);
}
public function testRegisterAttachmentService() {
$application = $this->createMock(Application::class);
$appContainer = $this->createMock(IAppContainer::class);
$fileServiceMock = $this->createMock(FileService::class);
$appContainer->expects($this->at(1))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($fileServiceMock);
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId);
$attachmentService->registerAttachmentService('custom', MyAttachmentService::class);
$this->assertEquals($fileServiceMock, $attachmentService->getService('deck_file'));
$this->assertEquals(MyAttachmentService::class, get_class($attachmentService->getService('custom')));
}
/**
* @expectedException \OCA\Deck\InvalidAttachmentType
*/
public function testRegisterAttachmentServiceNotExisting() {
$application = $this->createMock(Application::class);
$appContainer = $this->createMock(IAppContainer::class);
$fileServiceMock = $this->createMock(FileService::class);
$appContainer->expects($this->at(0))->method('query')->with(FileService::class)->willReturn($fileServiceMock);
$appContainer->expects($this->at(1))->method('query')->with(MyAttachmentService::class)->willReturn(new MyAttachmentService());
$application->expects($this->any())
->method('getContainer')
->willReturn($appContainer);
$attachmentService = new AttachmentService($this->attachmentMapper, $this->cardMapper, $this->permissionService, $application, $this->cacheFactory, $this->userId);
$attachmentService->registerAttachmentService('custom', MyAttachmentService::class);
$attachmentService->getService('deck_file_invalid');
}
private function mockPermission($permission) {
$this->permissionService->expects($this->once())
->method('checkPermission')
->with($this->cardMapper, 123, $permission);
}
private function createAttachment($type, $data) {
$attachment = new Attachment();
$attachment->setType($type);
$attachment->setData($data);
return $attachment;
}
public function testFindAll() {
$this->mockPermission(Acl::PERMISSION_READ);
$attachments = [
$this->createAttachment('deck_file','file1'),
$this->createAttachment('deck_file','file2'),
$this->createAttachment('deck_file_invalid','file3'),
];
$this->attachmentMapper->expects($this->once())
->method('findAll')
->with(123)
->willReturn($attachments);
$this->attachmentServiceImpl->expects($this->at(0))
->method('extendData')
->with($attachments[0]);
$this->attachmentServiceImpl->expects($this->at(1))
->method('extendData')
->with($attachments[1]);
$this->assertEquals($attachments, $this->attachmentService->findAll(123, false));
}
public function testFindAllWithDeleted() {
$this->mockPermission(Acl::PERMISSION_READ);
$attachments = [
$this->createAttachment('deck_file','file1'),
$this->createAttachment('deck_file','file2'),
$this->createAttachment('deck_file_invalid','file3'),
];
$attachmentsDeleted = [
$this->createAttachment('deck_file','file4'),
$this->createAttachment('deck_file','file5'),
$this->createAttachment('deck_file_invalid','file6'),
];
$this->attachmentMapper->expects($this->once())
->method('findAll')
->with(123)
->willReturn($attachments);
$this->attachmentMapper->expects($this->once())
->method('findToDelete')
->with(123, false)
->willReturn($attachmentsDeleted);
$this->attachmentServiceImpl->expects($this->at(0))
->method('extendData')
->with($attachments[0]);
$this->attachmentServiceImpl->expects($this->at(1))
->method('extendData')
->with($attachments[1]);
$this->assertEquals(array_merge($attachments, $attachmentsDeleted), $this->attachmentService->findAll(123, true));
}
public function testCount() {
$this->cache->expects($this->once())->method('get')->with('card-123')->willReturn(null);
$this->attachmentMapper->expects($this->once())->method('findAll')->willReturn([1,2,3,4]);
$this->cache->expects($this->once())->method('set')->with('card-123', 4)->willReturn(null);
$this->assertEquals(4, $this->attachmentService->count(123));
}
public function testCountCacheHit() {
$this->cache->expects($this->once())->method('get')->with('card-123')->willReturn(4);
$this->assertEquals(4, $this->attachmentService->count(123));
}
public function testCreate() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentServiceImpl->expects($this->once())
->method('create');
$this->attachmentMapper->expects($this->once())
->method('insert')
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('extendData')
->willReturnCallback(function($a) { $a->setExtendedData(['mime' => 'image/jpeg']); });
$actual = $this->attachmentService->create(123, 'deck_file', 'file_name.jpg');
$expected->setExtendedData(['mime' => 'image/jpeg']);
$this->assertEquals($expected, $actual);
}
public function testDisplay() {
$attachment = $this->createAttachment('deck_file', 'filename');
$response = new Response();
$this->mockPermission(Acl::PERMISSION_READ);
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('display')
->with($attachment)
->willReturn($response);
$actual = $this->attachmentService->display(123, 1);
$this->assertEquals($response, $actual);
}
/**
* @expectedException \OCA\Deck\NotFoundException
*/
public function testDisplayInvalid() {
$attachment = $this->createAttachment('deck_file', 'filename');
$response = new Response();
$this->mockPermission(Acl::PERMISSION_READ);
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('display')
->with($attachment)
->will($this->throwException(new InvalidAttachmentType('deck_file')));
$this->attachmentService->display(123, 1);
}
public function testUpdate() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('update');
$this->attachmentMapper->expects($this->once())
->method('update')
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('extendData')
->willReturnCallback(function($a) { $a->setExtendedData(['mime' => 'image/jpeg']); });
$actual = $this->attachmentService->update(123, 1, 'file_name.jpg');
$expected->setExtendedData(['mime' => 'image/jpeg']);
$expected->setLastModified($attachment->getLastModified());
$this->assertEquals($expected, $actual);
}
public function testDelete() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('allowUndo')
->willReturn(false);
$this->attachmentServiceImpl->expects($this->once())
->method('delete');
$this->attachmentMapper->expects($this->once())
->method('delete')
->willReturn($attachment);
$actual = $this->attachmentService->delete(123, 1);
$this->assertEquals($expected, $actual);
}
public function testDeleteWithUndo() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('allowUndo')
->willReturn(true);
$this->attachmentServiceImpl->expects($this->once())
->method('markAsDeleted')
->willReturnCallback(function($a) { $a->setDeletedAt(23); });
$this->attachmentMapper->expects($this->once())
->method('update')
->willReturn($attachment);
$expected->setDeletedAt(23);
$actual = $this->attachmentService->delete(123, 1);
$this->assertEquals($expected, $actual);
}
public function testRestore() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('allowUndo')
->willReturn(true);
$this->attachmentMapper->expects($this->once())
->method('update')
->willReturn($attachment);
$expected->setDeletedAt(0);
$actual = $this->attachmentService->restore(123, 1);
$this->assertEquals($expected, $actual);
}
/**
* @expectedException \OCA\Deck\NoPermissionException
*/
public function testRestoreNotAllowed() {
$attachment = $this->createAttachment('deck_file', 'file_name.jpg');
$expected = $this->createAttachment('deck_file', 'file_name.jpg');
$this->mockPermission(Acl::PERMISSION_EDIT);
$this->cache->expects($this->once())->method('clear')->with('card-123');
$this->attachmentMapper->expects($this->once())
->method('find')
->with(1)
->willReturn($attachment);
$this->attachmentServiceImpl->expects($this->once())
->method('allowUndo')
->willReturn(false);
$actual = $this->attachmentService->restore(123, 1);
}
}

View File

@@ -35,17 +35,17 @@ use Test\TestCase;
class CardServiceTest extends TestCase {
/** @var CardService|\PHPUnit_Framework_MockObject_MockObject */
/** @var CardService|\PHPUnit\Framework\MockObject\MockObject */
private $cardService;
/** @var CardMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var CardMapper|\PHPUnit\Framework\MockObject\MockObject */
private $cardMapper;
/** @var StackMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var StackMapper|\PHPUnit\Framework\MockObject\MockObject */
private $stackMapper;
/** @var PermissionService|\PHPUnit_Framework_MockObject_MockObject */
/** @var PermissionService|\PHPUnit\Framework\MockObject\MockObject */
private $permissionService;
/** @var AssignedUsersMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var AssignedUsersMapper|\PHPUnit\Framework\MockObject\MockObject */
private $assignedUsersMapper;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
/** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */
private $boardService;
public function setUp() {
@@ -55,7 +55,8 @@ class CardServiceTest extends TestCase {
$this->permissionService = $this->createMock(PermissionService::class);
$this->boardService = $this->createMock(BoardService::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->cardService = new CardService($this->cardMapper, $this->stackMapper, $this->permissionService, $this->boardService, $this->assignedUsersMapper);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->cardService = new CardService($this->cardMapper, $this->stackMapper, $this->permissionService, $this->boardService, $this->assignedUsersMapper, $this->attachmentService);
}
public function testFind() {

View File

@@ -0,0 +1,302 @@
<?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\Service;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Db\AssignedUsers;
use OCA\Deck\Db\AssignedUsersMapper;
use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\IAppContainer;
use OCP\Files\IAppData;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\ICacheFactory;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class FileServiceTest extends TestCase {
/** @var IL10N|MockObject */
private $l10n;
/** @var IAppData|MockObject */
private $appData;
/** @var IRequest|MockObject */
private $request;
/** @var ILogger|MockObject */
private $logger;
/** @var FileService */
private $fileService;
public function setUp() {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->appData = $this->createMock(IAppData::class);
$this->l10n = $this->createMock(IL10N::class);
$this->logger = $this->createMock(ILogger::class);
$this->fileService = new FileService($this->l10n, $this->appData, $this->request, $this->logger);
}
public function mockGetFolder($cardId) {
$folder = $this->createMock(ISimpleFolder::class);
$this->appData->expects($this->once())
->method('getFolder')
->with('file-card-' . $cardId)
->willReturn($folder);
return $folder;
}
public function mockGetFolderFailure($cardId) {
$folder = $this->createMock(ISimpleFolder::class);
$this->appData->expects($this->once())
->method('getFolder')
->with('file-card-' . $cardId)
->will($this->throwException(new \OCP\Files\NotFoundException()));
$this->appData->expects($this->once())
->method('newFolder')
->with('file-card-' . $cardId)
->willReturn($folder);
return $folder;
}
private function getAttachment() {
$attachment = new Attachment();
$attachment->setId(1);
$attachment->setCardId(123);
return $attachment;
}
private function mockGetUploadedFileEmpty() {
$this->request->expects($this->once())
->method('getUploadedFile')
->willReturn([]);
}
private function mockGetUploadedFileError($error) {
$this->request->expects($this->once())
->method('getUploadedFile')
->willReturn(['error' => $error]);
}
private function mockGetUploadedFile() {
$this->request->expects($this->once())
->method('getUploadedFile')
->willReturn([
'name' => 'file.jpg',
'tmp_name' => __FILE__,
]);
}
public function testExtendDataNotFound() {
$attachment = $this->getAttachment();
$folder = $this->mockGetFolder(123);
$folder->expects($this->once())->method('getFile')->will($this->throwException(new \OCP\Files\NotFoundException()));
$this->assertEquals($attachment, $this->fileService->extendData($attachment));
}
public function testExtendDataNotPermitted() {
$attachment = $this->getAttachment();
$folder = $this->mockGetFolder(123);
$folder->expects($this->once())->method('getFile')->will($this->throwException(new \OCP\Files\NotPermittedException()));
$this->assertEquals($attachment, $this->fileService->extendData($attachment));
}
public function testExtendData() {
$attachment = $this->getAttachment();
$expected = $this->getAttachment();
$expected->setExtendedData([
'filesize' => 100,
'mimetype' => 'image/jpeg',
'info' => pathinfo(__FILE__)
]);
$file = $this->createMock(ISimpleFile::class);
$file->expects($this->once())->method('getSize')->willReturn(100);
$file->expects($this->once())->method('getMimeType')->willReturn('image/jpeg');
$file->expects($this->once())->method('getName')->willReturn(__FILE__);
$folder = $this->mockGetFolder(123);
$folder->expects($this->once())->method('getFile')->willReturn($file);
$this->assertEquals($expected, $this->fileService->extendData($attachment));
}
/**
* @expectedException \Exception
*/
public function testCreateEmpty() {
$attachment = $this->getAttachment();
$this->l10n->expects($this->any())
->method('t')
->willReturn('Error');
$this->mockGetUploadedFileEmpty();
$this->fileService->create($attachment);
}
/**
* @expectedException \Exception
*/
public function testCreateError() {
$attachment = $this->getAttachment();
$this->mockGetUploadedFileError(UPLOAD_ERR_INI_SIZE);
$this->l10n->expects($this->any())
->method('t')
->willReturn('Error');
$this->fileService->create($attachment);
}
public function testCreate() {
$attachment = $this->getAttachment();
$this->mockGetUploadedFile();
$folder = $this->mockGetFolder(123);
$folder->expects($this->once())
->method('fileExists')
->willReturn(false);
$file = $this->createMock(ISimpleFile::class);
$file->expects($this->once())
->method('putContent')
->with(file_get_contents(__FILE__, 'r'));
$folder->expects($this->once())
->method('newFile')
->willReturn($file);
$this->fileService->create($attachment);
}
public function testCreateNoFolder() {
$attachment = $this->getAttachment();
$this->mockGetUploadedFile();
$folder = $this->mockGetFolderFailure(123);
$folder->expects($this->once())
->method('fileExists')
->willReturn(false);
$file = $this->createMock(ISimpleFile::class);
$file->expects($this->once())
->method('putContent')
->with(file_get_contents(__FILE__, 'r'));
$folder->expects($this->once())
->method('newFile')
->willReturn($file);
$this->fileService->create($attachment);
}
/**
* @expectedException \Exception
* @expectedExceptionMessage File already exists.
*/
public function testCreateExists() {
$attachment = $this->getAttachment();
$this->mockGetUploadedFile();
$folder = $this->mockGetFolder(123);
$folder->expects($this->once())
->method('fileExists')
->willReturn(true);
$this->fileService->create($attachment);
}
public function testUpdate() {
$attachment = $this->getAttachment();
$this->mockGetUploadedFile();
$folder = $this->mockGetFolder(123);
$file = $this->createMock(ISimpleFile::class);
$file->expects($this->once())
->method('putContent')
->with(file_get_contents(__FILE__, 'r'));
$folder->expects($this->once())
->method('getFile')
->willReturn($file);
$this->fileService->update($attachment);
}
public function testDelete() {
$attachment = $this->getAttachment();
$file = $this->createMock(ISimpleFile::class);
$folder = $this->mockGetFolder('123');
$folder->expects($this->once())
->method('getFile')
->willReturn($file);
$file->expects($this->once())
->method('delete');
$this->fileService->delete($attachment);
}
public function testDisplay() {
$attachment = $this->getAttachment();
$file = $this->createMock(ISimpleFile::class);
$folder = $this->mockGetFolder('123');
$folder->expects($this->once())
->method('getFile')
->willReturn($file);
$file->expects($this->exactly(2))
->method('getMimeType')
->willReturn('image/jpeg');
$actual = $this->fileService->display($attachment);
$expected = new FileDisplayResponse($file);
$expected->addHeader('Content-Type', 'image/jpeg');
$this->assertEquals($expected, $actual);
}
public function testDisplayPdf() {
$attachment = $this->getAttachment();
$file = $this->createMock(ISimpleFile::class);
$folder = $this->mockGetFolder('123');
$folder->expects($this->once())
->method('getFile')
->willReturn($file);
$file->expects($this->exactly(2))
->method('getMimeType')
->willReturn('application/pdf');
$actual = $this->fileService->display($attachment);
$expected = new FileDisplayResponse($file);
$expected->addHeader('Content-Type', 'application/pdf');
$policy = new ContentSecurityPolicy();
$policy->addAllowedObjectDomain('\'self\'');
$policy->addAllowedObjectDomain('blob:');
$expected->setContentSecurityPolicy($policy);
$this->assertEquals($expected, $actual);
}
public function testAllowUndo() {
$this->assertTrue($this->fileService->allowUndo());
}
public function testMarkAsDeleted() {
// TODO: use proper ITimeFactory in the service so we can mock the call to time
$attachment = $this->getAttachment();
$this->assertEquals(0, $attachment->getDeletedAt());
$this->fileService->markAsDeleted($attachment);
$this->assertGreaterThan(0, $attachment->getDeletedAt());
}
}

View File

@@ -30,13 +30,13 @@ use Test\TestCase;
class LabelServiceTest extends TestCase {
/** @var LabelMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var LabelMapper|\PHPUnit\Framework\MockObject\MockObject */
private $labelMapper;
/** @var PermissionService|\PHPUnit_Framework_MockObject_MockObject */
/** @var PermissionService|\PHPUnit\Framework\MockObject\MockObject */
private $permissionService;
/** @var LabelService */
private $labelService;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
/** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */
private $boardService;
public function setUp() {

View File

@@ -37,7 +37,7 @@ use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
class PermissionServiceTest extends \PHPUnit_Framework_TestCase {
class PermissionServiceTest extends \Test\TestCase {
/** @var PermissionService*/
private $service;

View File

@@ -44,17 +44,19 @@ class StackServiceTest extends TestCase {
/** @var StackService */
private $stackService;
/** @var \PHPUnit_Framework_MockObject_MockObject|StackMapper */
/** @var \PHPUnit\Framework\MockObject\MockObject|StackMapper */
private $stackMapper;
/** @var \PHPUnit_Framework_MockObject_MockObject|CardMapper */
/** @var \PHPUnit\Framework\MockObject\MockObject|CardMapper */
private $cardMapper;
/** @var \PHPUnit_Framework_MockObject_MockObject|LabelMapper */
/** @var \PHPUnit\Framework\MockObject\MockObject|LabelMapper */
private $labelMapper;
/** @var \PHPUnit_Framework_MockObject_MockObject|PermissionService */
/** @var \PHPUnit\Framework\MockObject\MockObject|PermissionService */
private $permissionService;
/** @var AssignedUsersMapper|\PHPUnit_Framework_MockObject_MockObject */
/** @var AssignedUsersMapper|\PHPUnit\Framework\MockObject\MockObject */
private $assignedUsersMapper;
/** @var BoardService|\PHPUnit_Framework_MockObject_MockObject */
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var BoardService|\PHPUnit\Framework\MockObject\MockObject */
private $boardService;
public function setUp() {
@@ -65,6 +67,7 @@ class StackServiceTest extends TestCase {
$this->permissionService = $this->createMock(PermissionService::class);
$this->boardService = $this->createMock(BoardService::class);
$this->assignedUsersMapper = $this->createMock(AssignedUsersMapper::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->stackService = new StackService(
$this->stackMapper,
@@ -72,7 +75,8 @@ class StackServiceTest extends TestCase {
$this->labelMapper,
$this->permissionService,
$this->boardService,
$this->assignedUsersMapper
$this->assignedUsersMapper,
$this->attachmentService
);
}

View File

@@ -0,0 +1,105 @@
<?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\Controller;
use OCA\Deck\Db\Acl;
use OCA\Deck\Service\AttachmentService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\LabelService;
use OCA\Deck\Service\StackService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class AttachmentControllerTest extends \Test\TestCase {
/** @var Controller|\PHPUnit\Framework\MockObject\MockObject */
private $controller;
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var AttachmentService|\PHPUnit\Framework\MockObject\MockObject */
private $attachmentService;
/** @var string */
private $userId = 'user';
public function setUp() {
$this->request = $this->createMock(IRequest::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->controller = new AttachmentController(
'deck',
$this->request,
$this->attachmentService,
$this->userId
);
}
public function testGetAll() {
$this->attachmentService->expects($this->once())->method('findAll')->with(1);
$this->controller->getAll(1);
}
public function testDisplay() {
$this->attachmentService->expects($this->once())->method('display')->with(1, 2);
$this->controller->display(1, 2);
}
public function testCreate() {
$this->request->expects($this->exactly(2))
->method('getParam')
->will($this->onConsecutiveCalls('type', 'data'));
$this->attachmentService->expects($this->once())
->method('create')
->with(1, 'type', 'data')
->willReturn(1);
$this->assertEquals(1, $this->controller->create(1));
}
public function testUpdate() {
$this->request->expects($this->exactly(1))
->method('getParam')
->will($this->onConsecutiveCalls('data'));
$this->attachmentService->expects($this->once())
->method('update')
->with(1, 2, 'data')
->willReturn(1);
$this->assertEquals(1, $this->controller->update(1, 2));
}
public function testDelete() {
$this->attachmentService->expects($this->once())
->method('delete')
->with(123, 234)
->willReturn(1);
$this->assertEquals(1, $this->controller->delete(123, 234));
}
public function testRestore() {
$this->attachmentService->expects($this->once())
->method('restore')
->with(123, 234)
->willReturn(1);
$this->assertEquals(1, $this->controller->restore(123, 234));
}
}

View File

@@ -26,7 +26,7 @@ namespace OCA\Deck\Controller;
use OCA\Deck\Db\Acl;
use OCP\IUser;
class BoardControllerTest extends \PHPUnit_Framework_TestCase {
class BoardControllerTest extends \Test\TestCase {
private $controller;
private $request;

View File

@@ -27,13 +27,13 @@ use OCA\Deck\Service\CardService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class CardControllerTest extends \PHPUnit_Framework_TestCase {
class CardControllerTest extends \Test\TestCase {
/** @var CardController|\PHPUnit_Framework_MockObject_MockObject */
/** @var CardController|\PHPUnit\Framework\MockObject\MockObject */
private $controller;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var CardService|\PHPUnit_Framework_MockObject_MockObject */
/** @var CardService|\PHPUnit\Framework\MockObject\MockObject */
private $cardService;
/** @var string */
private $userId = 'user';

View File

@@ -29,13 +29,13 @@ use OCA\Deck\Service\LabelService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class LabelControllerTest extends \PHPUnit_Framework_TestCase {
class LabelControllerTest extends \Test\TestCase {
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject */
/** @var Controller|\PHPUnit\Framework\MockObject\MockObject */
private $controller;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var LabelService|\PHPUnit_Framework_MockObject_MockObject */
/** @var LabelService|\PHPUnit\Framework\MockObject\MockObject */
private $labelService;
/** @var string */
private $userId = 'user';

View File

@@ -25,7 +25,7 @@ namespace OCA\Deck\Controller;
use PHPUnit_Framework_TestCase;
class PageControllerTest extends \PHPUnit_Framework_TestCase {
class PageControllerTest extends \Test\TestCase {
private $controller;
private $request;

View File

@@ -30,13 +30,13 @@ use OCA\Deck\Service\StackService;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class StackControllerTest extends \PHPUnit_Framework_TestCase {
class StackControllerTest extends \Test\TestCase {
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject */
/** @var Controller|\PHPUnit\Framework\MockObject\MockObject */
private $controller;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
/** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
private $request;
/** @var StackService|\PHPUnit_Framework_MockObject_MockObject */
/** @var StackService|\PHPUnit\Framework\MockObject\MockObject */
private $stackService;
/** @var string */
private $userId = 'user';