Merge pull request #14180 from nextcloud/feature/sharing-recommendations

Show sharing recommendations
This commit is contained in:
Roeland Jago Douma 2019-02-25 09:52:50 +01:00 committed by GitHub
commit d48cc08fc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 469 additions and 2 deletions

View file

@ -94,6 +94,11 @@ return [
'url' => '/api/v1/sharees',
'verb' => 'GET',
],
[
'name' => 'ShareesAPI#findRecommended',
'url' => '/api/v1/sharees_recommended',
'verb' => 'GET',
],
/*
* Remote Shares
*/

View file

@ -29,17 +29,29 @@ declare(strict_types=1);
*/
namespace OCA\Files_Sharing\Controller;
use function array_filter;
use function array_slice;
use function array_values;
use Generator;
use OC\Collaboration\Collaborators\SearchResult;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCSController;
use OCP\Collaboration\Collaborators\ISearch;
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Collaboration\Collaborators\SearchResultType;
use OCP\IRequest;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\Share;
use OCP\Share\IManager;
use function usort;
class ShareesAPIController extends OCSController {
/** @var userId */
protected $userId;
/** @var IConfig */
protected $config;
@ -87,6 +99,7 @@ class ShareesAPIController extends OCSController {
private $collaboratorSearch;
/**
* @param string $UserId
* @param string $appName
* @param IRequest $request
* @param IConfig $config
@ -95,6 +108,7 @@ class ShareesAPIController extends OCSController {
* @param ISearch $collaboratorSearch
*/
public function __construct(
$UserId,
string $appName,
IRequest $request,
IConfig $config,
@ -103,7 +117,7 @@ class ShareesAPIController extends OCSController {
ISearch $collaboratorSearch
) {
parent::__construct($appName, $request);
$this->userId = $UserId;
$this->config = $config;
$this->urlGenerator = $urlGenerator;
$this->shareManager = $shareManager;
@ -212,6 +226,148 @@ class ShareesAPIController extends OCSController {
return $response;
}
/**
* @param string $user
* @param int $shareType
*
* @return Generator<array<string>>
*/
private function getAllShareesByType(string $user, int $shareType): Generator {
$offset = 0;
$pageSize = 50;
while (count($page = $this->shareManager->getSharesBy(
$user,
$shareType,
null,
false,
$pageSize,
$offset
))) {
foreach ($page as $share) {
yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
}
$offset += $pageSize;
}
}
private function sortShareesByFrequency(array $sharees): array {
usort($sharees, function(array $s1, array $s2) {
return $s2['count'] - $s1['count'];
});
return $sharees;
}
private $searchResultTypeMap = [
Share::SHARE_TYPE_USER => 'users',
Share::SHARE_TYPE_GROUP => 'groups',
Share::SHARE_TYPE_REMOTE => 'remotes',
Share::SHARE_TYPE_REMOTE_GROUP => 'remote_groups',
Share::SHARE_TYPE_EMAIL => 'emails',
];
private function getAllSharees(string $user, array $shareTypes): ISearchResult {
$result = [];
foreach ($shareTypes as $shareType) {
$sharees = $this->getAllShareesByType($user, $shareType);
$shareTypeResults = [];
foreach ($sharees as list($sharee, $displayname)) {
if (!isset($this->searchResultTypeMap[$shareType])) {
continue;
}
if (!isset($shareTypeResults[$sharee])) {
$shareTypeResults[$sharee] = [
'count' => 1,
'label' => $displayname,
'value' => [
'shareType' => $shareType,
'shareWith' => $sharee,
],
];
} else {
$shareTypeResults[$sharee]['count']++;
}
}
$result = array_merge($result, array_values($shareTypeResults));
}
$top5 = array_slice(
$this->sortShareesByFrequency($result),
0,
5
);
$searchResult = new SearchResult();
foreach ($this->searchResultTypeMap as $int => $str) {
$searchResult->addResultSet(new SearchResultType($str), [], []);
foreach ($top5 as $x) {
if ($x['value']['shareType'] === $int) {
$searchResult->addResultSet(new SearchResultType($str), [], [$x]);
}
}
}
return $searchResult;
}
/**
* @NoAdminRequired
*
* @param string $itemType
* @return DataResponse
* @throws OCSBadRequestException
*/
public function findRecommended(string $itemType = null, $shareType = null): DataResponse {
$shareTypes = [
Share::SHARE_TYPE_USER,
];
if ($itemType === null) {
throw new OCSBadRequestException('Missing itemType');
} elseif ($itemType === 'file' || $itemType === 'folder') {
if ($this->shareManager->allowGroupSharing()) {
$shareTypes[] = Share::SHARE_TYPE_GROUP;
}
if ($this->isRemoteSharingAllowed($itemType)) {
$shareTypes[] = Share::SHARE_TYPE_REMOTE;
}
if ($this->isRemoteGroupSharingAllowed($itemType)) {
$shareTypes[] = Share::SHARE_TYPE_REMOTE_GROUP;
}
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_EMAIL)) {
$shareTypes[] = Share::SHARE_TYPE_EMAIL;
}
if ($this->shareManager->shareProviderExists(Share::SHARE_TYPE_ROOM)) {
$shareTypes[] = Share::SHARE_TYPE_ROOM;
}
} else {
$shareTypes[] = Share::SHARE_TYPE_GROUP;
$shareTypes[] = Share::SHARE_TYPE_EMAIL;
}
// FIXME: DI
if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
$shareTypes[] = Share::SHARE_TYPE_CIRCLE;
}
if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
$shareTypes = array_intersect($shareTypes, $_GET['shareType']);
sort($shareTypes);
} else if (is_numeric($shareType)) {
$shareTypes = array_intersect($shareTypes, [(int) $shareType]);
sort($shareTypes);
}
return new DataResponse(
$this->getAllSharees($this->userId, $shareTypes)->asArray()
);
}
/**
* Method to get out the static call for better testing
*

View file

@ -50,6 +50,9 @@ class ShareesAPIControllerTest extends TestCase {
/** @var ShareesAPIController */
protected $sharees;
/** @var string */
protected $uid;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
protected $request;
@ -62,6 +65,7 @@ class ShareesAPIControllerTest extends TestCase {
protected function setUp() {
parent::setUp();
$this->uid = 'test123';
$this->request = $this->createMock(IRequest::class);
$this->shareManager = $this->createMock(IManager::class);
@ -74,6 +78,7 @@ class ShareesAPIControllerTest extends TestCase {
$this->collaboratorSearch = $this->createMock(ISearch::class);
$this->sharees = new ShareesAPIController(
$this->uid,
'files_sharing',
$this->request,
$configMock,
@ -243,6 +248,8 @@ class ShareesAPIControllerTest extends TestCase {
->method('allowGroupSharing')
->willReturn($allowGroupSharing);
/** @var string */
$uid = 'test123';
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */
$request = $this->createMock(IRequest::class);
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */
@ -251,6 +258,7 @@ class ShareesAPIControllerTest extends TestCase {
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController')
->setConstructorArgs([
$uid,
'files_sharing',
$request,
$config,
@ -335,6 +343,8 @@ class ShareesAPIControllerTest extends TestCase {
$config->expects($this->never())
->method('getAppValue');
/** @var string */
$uid = 'test123';
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject $request */
$request = $this->createMock(IRequest::class);
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject $urlGenerator */
@ -343,6 +353,7 @@ class ShareesAPIControllerTest extends TestCase {
/** @var \PHPUnit_Framework_MockObject_MockObject|\OCA\Files_Sharing\Controller\ShareesAPIController $sharees */
$sharees = $this->getMockBuilder('\OCA\Files_Sharing\Controller\ShareesAPIController')
->setConstructorArgs([
$uid,
'files_sharing',
$request,
$config,

Binary file not shown.

Binary file not shown.

View file

@ -50,6 +50,9 @@
/** @type {object} **/
_lastSuggestions: undefined,
/** @type {object} **/
_lastRecommendations: undefined,
/** @type {int} **/
_pendingOperationsCount: 0,
@ -382,7 +385,299 @@
return this._lastSuggestions.promise;
},
_getRecommendations: function(model) {
if (this._lastRecommendations &&
this._lastRecommendations.model === model) {
return this._lastRecommendations.promise;
}
var deferred = $.Deferred();
$.get(
OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees_recommended',
{
format: 'json',
itemType: model.get('itemType')
},
function (result) {
if (result.ocs.meta.statuscode === 100) {
var filter = function(users, groups, remotes, remote_groups, emails, circles, rooms) {
if (typeof(emails) === 'undefined') {
emails = [];
}
if (typeof(circles) === 'undefined') {
circles = [];
}
if (typeof(rooms) === 'undefined') {
rooms = [];
}
var usersLength;
var groupsLength;
var remotesLength;
var remoteGroupsLength;
var emailsLength;
var circlesLength;
var roomsLength;
var i, j;
//Filter out the current user
usersLength = users.length;
for (i = 0; i < usersLength; i++) {
if (users[i].value.shareWith === OC.currentUser) {
users.splice(i, 1);
break;
}
}
// Filter out the owner of the share
if (model.hasReshare()) {
usersLength = users.length;
for (i = 0 ; i < usersLength; i++) {
if (users[i].value.shareWith === model.getReshareOwner()) {
users.splice(i, 1);
break;
}
}
}
var shares = model.get('shares');
var sharesLength = shares.length;
// Now filter out all sharees that are already shared with
for (i = 0; i < sharesLength; i++) {
var share = shares[i];
if (share.share_type === OC.Share.SHARE_TYPE_USER) {
usersLength = users.length;
for (j = 0; j < usersLength; j++) {
if (users[j].value.shareWith === share.share_with) {
users.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_GROUP) {
groupsLength = groups.length;
for (j = 0; j < groupsLength; j++) {
if (groups[j].value.shareWith === share.share_with) {
groups.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE) {
remotesLength = remotes.length;
for (j = 0; j < remotesLength; j++) {
if (remotes[j].value.shareWith === share.share_with) {
remotes.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_REMOTE_GROUP) {
remoteGroupsLength = remote_groups.length;
for (j = 0; j < remoteGroupsLength; j++) {
if (remote_groups[j].value.shareWith === share.share_with) {
remote_groups.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
emailsLength = emails.length;
for (j = 0; j < emailsLength; j++) {
if (emails[j].value.shareWith === share.share_with) {
emails.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_CIRCLE) {
circlesLength = circles.length;
for (j = 0; j < circlesLength; j++) {
if (circles[j].value.shareWith === share.share_with) {
circles.splice(j, 1);
break;
}
}
} else if (share.share_type === OC.Share.SHARE_TYPE_ROOM) {
roomsLength = rooms.length;
for (j = 0; j < roomsLength; j++) {
if (rooms[j].value.shareWith === share.share_with) {
rooms.splice(j, 1);
break;
}
}
}
}
};
filter(
result.ocs.data.exact.users,
result.ocs.data.exact.groups,
result.ocs.data.exact.remotes,
result.ocs.data.exact.remote_groups,
result.ocs.data.exact.emails,
result.ocs.data.exact.circles,
result.ocs.data.exact.rooms
);
var exactUsers = result.ocs.data.exact.users;
var exactGroups = result.ocs.data.exact.groups;
var exactRemotes = result.ocs.data.exact.remotes || [];
var exactRemoteGroups = result.ocs.data.exact.remote_groups || [];
var exactEmails = [];
if (typeof(result.ocs.data.emails) !== 'undefined') {
exactEmails = result.ocs.data.exact.emails;
}
var exactCircles = [];
if (typeof(result.ocs.data.circles) !== 'undefined') {
exactCircles = result.ocs.data.exact.circles;
}
var exactRooms = [];
if (typeof(result.ocs.data.rooms) !== 'undefined') {
exactRooms = result.ocs.data.exact.rooms;
}
var exactMatches = exactUsers.concat(exactGroups).concat(exactRemotes).concat(exactRemoteGroups).concat(exactEmails).concat(exactCircles).concat(exactRooms);
filter(
result.ocs.data.users,
result.ocs.data.groups,
result.ocs.data.remotes,
result.ocs.data.remote_groups,
result.ocs.data.emails,
result.ocs.data.circles,
result.ocs.data.rooms
);
var users = result.ocs.data.users;
var groups = result.ocs.data.groups;
var remotes = result.ocs.data.remotes || [];
var remoteGroups = result.ocs.data.remote_groups || [];
var lookup = result.ocs.data.lookup || [];
var emails = [];
if (typeof(result.ocs.data.emails) !== 'undefined') {
emails = result.ocs.data.emails;
}
var circles = [];
if (typeof(result.ocs.data.circles) !== 'undefined') {
circles = result.ocs.data.circles;
}
var rooms = [];
if (typeof(result.ocs.data.rooms) !== 'undefined') {
rooms = result.ocs.data.rooms;
}
var suggestions = exactMatches.concat(users).concat(groups).concat(remotes).concat(remoteGroups).concat(emails).concat(circles).concat(rooms).concat(lookup);
function dynamicSort(property) {
return function (a,b) {
var aProperty = '';
var bProperty = '';
if (typeof a[property] !== 'undefined') {
aProperty = a[property];
}
if (typeof b[property] !== 'undefined') {
bProperty = b[property];
}
return (aProperty < bProperty) ? -1 : (aProperty > bProperty) ? 1 : 0;
}
}
/**
* Sort share entries by uuid to properly group them
*/
var grouped = suggestions.sort(dynamicSort('uuid'));
var previousUuid = null;
var groupedLength = grouped.length;
var result = [];
/**
* build the result array that only contains all contact entries from
* merged contacts, if the search term matches its contact name
*/
for (var i = 0; i < groupedLength; i++) {
if (typeof grouped[i].uuid !== 'undefined' && grouped[i].uuid === previousUuid) {
grouped[i].merged = true;
}
if (typeof grouped[i].merged === 'undefined') {
result.push(grouped[i]);
}
previousUuid = grouped[i].uuid;
}
var moreResultsAvailable =
(
oc_config['sharing.maxAutocompleteResults'] > 0
&& Math.min(perPage, oc_config['sharing.maxAutocompleteResults'])
<= Math.max(
users.length + exactUsers.length,
groups.length + exactGroups.length,
remoteGroups.length + exactRemoteGroups.length,
remotes.length + exactRemotes.length,
emails.length + exactEmails.length,
circles.length + exactCircles.length,
rooms.length + exactRooms.length,
lookup.length
)
);
deferred.resolve(result, exactMatches, moreResultsAvailable);
} else {
deferred.reject(result.ocs.meta.message);
}
}
).fail(function() {
deferred.reject();
});
this._lastRecommendations = {
model: model,
promise: deferred.promise()
};
return this._lastRecommendations.promise;
},
recommendationHandler: function (response) {
var view = this;
var $shareWithField = $('.shareWithField');
this._getRecommendations(
view.model
).done(function(suggestions, exactMatches) {
view._pendingOperationsCount--;
if (view._pendingOperationsCount === 0) {
$loading.addClass('hidden');
$loading.removeClass('inlineblock');
$confirm.removeClass('hidden');
}
if (suggestions.length > 0) {
$shareWithField
.autocomplete("option", "autoFocus", true);
response(suggestions);
} else {
console.info('no sharing recommendations found');
response();
}
}).fail(function(message) {
view._pendingOperationsCount--;
if (view._pendingOperationsCount === 0) {
$loading.addClass('hidden');
$loading.removeClass('inlineblock');
$confirm.removeClass('hidden');
}
console.error('could not load recommendations', message)
});
},
autocompleteHandler: function (search, response) {
// If nothing is entered we show recommendations instead of search
// results
if (search.term.length === 0) {
this.recommendationHandler(response);
return;
}
var $shareWithField = $('.shareWithField'),
view = this,
$loading = this.$el.find('.shareWithLoading'),
@ -766,7 +1061,7 @@
};
$shareField.autocomplete({
minLength: 1,
minLength: 0,
delay: 750,
focus: function(event) {
event.preventDefault();