Skip to content

Commit 838e7ac

Browse files
authored
Merge pull request #4010 from nextcloud/enh/change-owner
Add transfering polls to another user
2 parents 90334ed + f1e2f69 commit 838e7ac

25 files changed

Lines changed: 671 additions & 320 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
2323
- Configuration for using the Nextcloud default footer in public polls
2424
- Delete polls without the need to archive them first
2525
- Collapsible poll description
26+
- Transfer polls to another owner by the current poll owner or the administration
2627

2728
## [7.4.1] - 2024-03-07
2829
### New

lib/AppInfo/Application.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
use OCA\Polls\Event\PollOwnerChangeEvent;
3232
use OCA\Polls\Event\PollReopenEvent;
3333
use OCA\Polls\Event\PollRestoredEvent;
34-
use OCA\Polls\Event\PollTakeoverEvent;
3534
use OCA\Polls\Event\PollUpdatedEvent;
3635
use OCA\Polls\Event\ShareChangedDisplayNameEvent;
3736
use OCA\Polls\Event\ShareChangedEmailEvent;
@@ -106,7 +105,6 @@ public function register(IRegistrationContext $context): void {
106105
$context->registerEventListener(PollOptionReorderedEvent::class, PollListener::class);
107106
$context->registerEventListener(PollOwnerChangeEvent::class, PollListener::class);
108107
$context->registerEventListener(PollRestoredEvent::class, PollListener::class);
109-
$context->registerEventListener(PollTakeoverEvent::class, PollListener::class);
110108
$context->registerEventListener(PollUpdatedEvent::class, PollListener::class);
111109
$context->registerEventListener(PollReopenEvent::class, PollListener::class);
112110
$context->registerEventListener(PollCloseEvent::class, PollListener::class);

lib/Controller/PollApiController.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,26 +159,26 @@ public function clone(int $pollId): DataResponse {
159159

160160
/**
161161
* Transfer all polls from one user to another (change owner of poll)
162-
* @param string $sourceUser User to transfer polls from
163-
* @param string $targetUser User to transfer polls to
162+
* @param string $sourceUserId User id to transfer polls from
163+
* @param string $targetUserId User id to transfer polls to
164164
*/
165165
#[CORS]
166166
#[NoCSRFRequired]
167-
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/transfer/{sourceUser}/{targetUser}', requirements: ['apiVersion' => '(v2)'])]
168-
public function transferPolls(string $sourceUser, string $targetUser): DataResponse {
169-
return $this->response(fn () => ['transferred' => $this->pollService->transferPolls($sourceUser, $targetUser)]);
167+
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/transfer/{sourceUserId}/{targetUserId}', requirements: ['apiVersion' => '(v2)'])]
168+
public function transferPolls(string $sourceUserId, string $targetUserId): DataResponse {
169+
return $this->response(fn () => ['transferred' => $this->pollService->transferPolls($sourceUserId, $targetUserId)]);
170170
}
171171

172172
/**
173173
* Transfer single poll to another user (change owner of poll)
174174
* @param int $pollId Poll to transfer
175-
* @param string $targetUser User to transfer the poll to
175+
* @param string $targetUserId User id to transfer the poll to
176176
*/
177177
#[CORS]
178178
#[NoCSRFRequired]
179-
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/{pollId}/transfer/{targetUser}', requirements: ['apiVersion' => '(v2)'])]
180-
public function transferPoll(int $pollId, string $targetUser): DataResponse {
181-
return $this->response(fn () => ['transferred' => $this->pollService->transferPoll($pollId, $targetUser)]);
179+
#[ApiRoute(verb: 'PUT', url: '/api/v1.0/poll/{pollId}/transfer/{targetUserId}', requirements: ['apiVersion' => '(v2)'])]
180+
public function transferPoll(int $pollId, string $targetUserId): DataResponse {
181+
return $this->response(fn () => ['transferred' => $this->pollService->transferPoll($pollId, $targetUserId)]);
182182
}
183183

184184
/**

lib/Controller/PollController.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,23 @@ private function clonePoll(int $pollId): Poll {
202202

203203
/**
204204
* Transfer polls between users
205-
* @param string $sourceUser User to transfer polls from
206-
* @param string $targetUser User to transfer polls to
205+
* @param string $sourceUserId User id to transfer polls from
206+
* @param string $targetUserId User id to transfer polls to
207207
*/
208-
#[FrontpageRoute(verb: 'PUT', url: '/poll/transfer/{sourceUser}/{targetUser}')]
209-
public function transferPolls(string $sourceUser, string $targetUser): JSONResponse {
210-
return $this->response(fn () => $this->pollService->transferPolls($sourceUser, $targetUser));
208+
#[FrontpageRoute(verb: 'PUT', url: '/poll/transfer/{sourceUserId}/{targetUserId}')]
209+
public function transferPolls(string $sourceUserId, string $targetUserId): JSONResponse {
210+
return $this->response(fn () => $this->pollService->transferPolls($sourceUserId, $targetUserId));
211+
}
212+
213+
/**
214+
* Transfer ownership of one poll
215+
* @param int $pollId poll to transfer ownership
216+
* @param string $targetUserId User to transfer polls to
217+
*/
218+
#[NoAdminRequired]
219+
#[FrontpageRoute(verb: 'PUT', url: '/poll/{pollId}/changeowner/{targetUserId}')]
220+
public function changeOwner(int $pollId, string $targetUserId): JSONResponse {
221+
return $this->response(fn () => $this->pollService->transferPoll($pollId, $targetUserId));
211222
}
212223

213224
/**

lib/Controller/SystemController.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ public function __construct(
3535
#[NoAdminRequired]
3636
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
3737
#[FrontpageRoute(verb: 'GET', url: '/search/users/{query}')]
38-
public function userSearch(string $query = ''): JSONResponse {
39-
return new JSONResponse(['siteusers' => $this->systemService->getSiteUsersAndGroups(
40-
$query)], Http::STATUS_OK);
38+
public function userSearch(string $query, string $types): JSONResponse {
39+
$types = explode(',', $types);
40+
return new JSONResponse([
41+
'siteusers' => $this->systemService->getSiteUsersAndGroups($query, $types),
42+
'types' => $types
43+
], Http::STATUS_OK);
4144
}
4245
/**
4346
* Get a combined list of all NC groups

lib/Db/Poll.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class Poll extends EntityWithUser implements JsonSerializable {
112112
public const PERMISSION_OVERRIDE = 'override_permission';
113113
public const PERMISSION_POLL_VIEW = 'view';
114114
public const PERMISSION_POLL_EDIT = 'edit';
115+
public const PERMISSION_POLL_CHANGE_OWNER = 'changeOwner';
115116
public const PERMISSION_POLL_DELETE = 'delete';
116117
public const PERMISSION_POLL_ARCHIVE = 'archive';
117118
public const PERMISSION_POLL_RESULTS_VIEW = 'seeResults';
@@ -291,6 +292,7 @@ public function getPermissionsArray(): array {
291292
'addSharesExternal' => $this->getIsAllowed(self::PERMISSION_SHARE_ADD_EXTERNAL),
292293
'archive' => $this->getIsAllowed(self::PERMISSION_POLL_ARCHIVE),
293294
'changeForeignVotes' => $this->getIsAllowed(self::PERMISSION_VOTE_FOREIGN_CHANGE),
295+
'changeOwner' => $this->getIsAllowed(self::PERMISSION_POLL_CHANGE_OWNER),
294296
'clone' => $this->getIsAllowed(self::PERMISSION_OPTION_CLONE),
295297
'comment' => $this->getIsAllowed(self::PERMISSION_COMMENT_ADD),
296298
'confirmOptions' => $this->getIsAllowed(self::PERMISSION_OPTION_CONFIRM),
@@ -302,6 +304,7 @@ public function getPermissionsArray(): array {
302304
'seeUsernames' => $this->getIsAllowed(self::PERMISSION_POLL_USERNAMES_VIEW),
303305
'shiftOptions' => $this->getIsAllowed(self::PERMISSION_OPTIONS_SHIFT),
304306
'subscribe' => $this->getIsAllowed(self::PERMISSION_POLL_SUBSCRIBE),
307+
'takeOver' => $this->getIsAllowed(self::PERMISSION_POLL_TAKEOVER),
305308
'view' => $this->getIsAllowed(self::PERMISSION_POLL_VIEW),
306309
'vote' => $this->getIsAllowed(self::PERMISSION_VOTE_EDIT),
307310
];
@@ -560,7 +563,8 @@ public function getIsAllowed(string $permission): bool {
560563
self::PERMISSION_POLL_EDIT => $this->getAllowEditPoll(),
561564
self::PERMISSION_POLL_DELETE => $this->getAllowDeletePoll(),
562565
self::PERMISSION_POLL_ARCHIVE => $this->getAllowEditPoll(),
563-
self::PERMISSION_POLL_TAKEOVER => $this->getAllowEditPoll(),
566+
self::PERMISSION_POLL_TAKEOVER => $this->getAllowTakeOver(),
567+
self::PERMISSION_POLL_CHANGE_OWNER => $this->getAllowChangeOwner(),
564568
self::PERMISSION_POLL_SUBSCRIBE => $this->getAllowSubscribeToPoll(),
565569
self::PERMISSION_POLL_RESULTS_VIEW => $this->getAllowShowResults(),
566570
self::PERMISSION_POLL_USERNAMES_VIEW => $this->getAllowEditPoll() || !$this->getAnonymous(),
@@ -669,6 +673,18 @@ private function getAllowEditPoll(): bool {
669673
return false;
670674
}
671675

676+
private function getAllowTakeOver(): bool {
677+
return $this->userSession->getCurrentUser()->getIsAdmin();
678+
}
679+
680+
/**
681+
* Checks, if user is allowed to edit the poll configuration
682+
**/
683+
private function getAllowChangeOwner(): bool {
684+
return $this->getAllowEditPoll()
685+
|| $this->userSession->getCurrentUser()->getIsAdmin();
686+
}
687+
672688
/**
673689
* Checks, if user is allowed to access (view) poll
674690
*/

lib/Event/PollOwnerChangeEvent.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,26 @@
88
namespace OCA\Polls\Event;
99

1010
use OCA\Polls\Db\Poll;
11+
use OCA\Polls\Notification\Notifier;
1112

1213
class PollOwnerChangeEvent extends PollEvent {
1314
public function __construct(
1415
protected Poll $poll,
16+
protected string $oldOwner,
17+
protected string $newOwner,
1518
) {
1619
parent::__construct($poll);
1720
$this->eventId = self::OWNER_CHANGE;
1821
}
22+
public function getNotification(): array {
23+
return [
24+
'msgId' => Notifier::NOTIFY_POLL_CHANGED_OWNER,
25+
'objectType' => 'poll',
26+
'objectValue' => $this->getPollId(),
27+
'recipient' => $this->oldOwner,
28+
'newOwner' => $this->newOwner,
29+
'actor' => $this->getActor(),
30+
'pollTitle' => $this->getPollTitle(),
31+
];
32+
}
1933
}

lib/Event/PollTakeoverEvent.php

Lines changed: 0 additions & 30 deletions
This file was deleted.

lib/Listener/BaseListener.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use OCA\Polls\Event\PollDeletedEvent;
1414
use OCA\Polls\Event\PollExpiredEvent;
1515
use OCA\Polls\Event\PollOwnerChangeEvent;
16-
use OCA\Polls\Event\PollTakeoverEvent;
1716
use OCA\Polls\Exceptions\InvalidClassException;
1817
use OCA\Polls\Exceptions\OCPEventException;
1918
use OCA\Polls\Model\Settings\AppSettings;
@@ -94,8 +93,7 @@ protected function checkClass() : void {
9493

9594
protected function updateLastInteraction(): void {
9695
// Update last interaction, exept event is one of the of excluded events
97-
if ($this->event instanceof PollTakeoverEvent
98-
|| $this->event instanceof PollOwnerChangeEvent
96+
if ($this->event instanceof PollOwnerChangeEvent
9997
|| $this->event instanceof PollExpiredEvent
10098
|| $this->event instanceof PollDeletedEvent
10199
|| $this->event instanceof PollArchivedEvent

lib/Notification/Notifier.php

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
use OCP\L10N\IFactory;
1818
use OCP\Notification\INotification;
1919
use OCP\Notification\INotifier;
20+
use Psr\Log\LoggerInterface;
2021

2122
class Notifier implements INotifier {
2223
public const NOTIFY_POLL_DELETED_BY_OTHER = 'deletePollByOther';
2324
public const NOTIFY_POLL_ARCHIVED_BY_OTHER = 'softDeletePollByOther';
2425
public const NOTIFY_POLL_TAKEOVER = 'takeOverPoll';
26+
public const NOTIFY_POLL_CHANGED_OWNER = 'PollChangedOwner';
2527
public const NOTIFY_INVITATION = 'invitation';
2628
private const SUBJECT_PARSED = 'parsedSubject';
2729
private const SUBJECT_RICH = 'richSubject';
@@ -33,6 +35,7 @@ public function __construct(
3335
protected PollMapper $pollMapper,
3436
private UserMapper $userMapper,
3537
private NotificationService $notificationService,
38+
private LoggerInterface $logger,
3639
) {
3740
}
3841

@@ -50,22 +53,6 @@ public function getName(): string {
5053
return $this->l10nFactory->get(AppConstants::APP_ID)->t('Polls');
5154
}
5255

53-
/**
54-
* @return string[][]
55-
*
56-
* @psalm-return array{actor: array{type: 'user', id: string, name: string}}
57-
*/
58-
private function getActor(string $actorId): array {
59-
$actor = $this->userMapper->getUserFromUserBase($actorId);
60-
return [
61-
'actor' => [
62-
'type' => 'user',
63-
'id' => $actor->getId(),
64-
'name' => $actor->getDisplayName(),
65-
]
66-
];
67-
}
68-
6956
public function prepare(INotification $notification, string $languageCode): INotification {
7057
$l = $this->l10nFactory->get(AppConstants::APP_ID, $languageCode);
7158
if ($notification->getApp() !== AppConstants::APP_ID) {
@@ -81,37 +68,72 @@ public function prepare(INotification $notification, string $languageCode): INot
8168

8269
try {
8370
$poll = $this->pollMapper->get(intval($notification->getObjectId()));
84-
$actor = $this->getActor($parameters['actor'] ?? $poll->getOwner());
85-
$pollTitle = $poll->getTitle();
86-
$notification->setLink($poll->getVoteUrl());
8771
} catch (DoesNotExistException $e) {
72+
$this->logger->info('Notification silently removed, poll not found', [
73+
'notification' => $notification->getObjectId(),
74+
'error' => $e->getMessage(),
75+
]);
8876
$this->notificationService->removeNotification(intval($notification->getObjectId()));
8977
return $notification;
9078
}
9179

80+
$actor = $this->userMapper->getUserFromUserBase($parameters['actor'] ?? $poll->getOwner());
81+
$pollTitle = $parameters['pollTitle'] ?? $poll->getTitle();
82+
$notification->setLink($poll->getVoteUrl());
83+
84+
// TODO: tidy subjects and parameters
9285
$subjects = match ($notification->getSubject()) {
9386
self::NOTIFY_INVITATION => [
94-
self::SUBJECT_PARSED => $l->t('%s invited you to a poll', $actor['actor']['name']),
87+
self::SUBJECT_PARSED => $l->t('%s invited you to a poll', $actor->getDisplayName()),
9588
self::SUBJECT_RICH => $l->t('{actor} has invited you to the poll "%s".', $pollTitle),
9689
],
9790
self::NOTIFY_POLL_TAKEOVER => [
98-
self::SUBJECT_PARSED => $l->t('%s took over your poll', $actor['actor']['name']),
91+
self::SUBJECT_PARSED => $l->t('%s took over your poll', $actor->getDisplayName()),
9992
self::SUBJECT_RICH => $l->t('{actor} took over your poll "%s" and is the new owner.', $pollTitle),
10093
],
94+
self::NOTIFY_POLL_CHANGED_OWNER => [
95+
self::SUBJECT_PARSED => $l->t('%s is the new owner of your poll. ', $parameters['newOwner']),
96+
self::SUBJECT_RICH => $l->t('{actor} transfered your poll "%s" to {newOwner}. You are no more the owner.', $pollTitle),
97+
],
10198
self::NOTIFY_POLL_DELETED_BY_OTHER => [
102-
self::SUBJECT_PARSED => $l->t('%s deleted your poll', $actor['actor']['name']),
99+
self::SUBJECT_PARSED => $l->t('%s deleted your poll', $actor->getDisplayName()),
103100
self::SUBJECT_RICH => $l->t('{actor} deleted your poll "%s".', $pollTitle),
104101
],
105102
self::NOTIFY_POLL_ARCHIVED_BY_OTHER => [
106-
self::SUBJECT_PARSED => $l->t('%s archived your poll', $actor['actor']['name']),
103+
self::SUBJECT_PARSED => $l->t('%s archived your poll', $actor->getDisplayName()),
107104
self::SUBJECT_RICH => $l->t('{actor} archived your poll "%s".', $pollTitle),
108105
],
109106
// Unknown subject => Unknown notification => throw
110107
default => throw new \InvalidArgumentException(),
111108
};
112109

113-
$notification->setParsedSubject($subjects[self::SUBJECT_PARSED]);
114-
$notification->setRichSubject($subjects[self::SUBJECT_RICH], $actor);
110+
switch ($notification->getSubject()) {
111+
case self::NOTIFY_POLL_CHANGED_OWNER:
112+
$newOwner = $this->userMapper->getUserFromUserBase($parameters['newOwner']);
113+
// overwrite the subject with the new owner
114+
$notification->setParsedSubject(
115+
$l->t('%s is the new owner of your poll. ', $newOwner->getDisplayName())
116+
);
117+
118+
$notification->setRichSubject(
119+
$subjects[self::SUBJECT_RICH],
120+
[
121+
'actor' => $actor->getRichObjectString(),
122+
'newOwner' => $newOwner->getRichObjectString(),
123+
]
124+
);
125+
break;
126+
127+
default:
128+
$notification->setParsedSubject($subjects[self::SUBJECT_PARSED]);
129+
$notification->setRichSubject(
130+
$subjects[self::SUBJECT_RICH],
131+
[
132+
'actor' => $actor->getRichObjectString(),
133+
]
134+
);
135+
break;
136+
}
115137

116138
return $notification;
117139
}

0 commit comments

Comments
 (0)