Signed-off-by: dartcafe <github@dartcafe.de>
This commit is contained in:
dartcafe 2022-08-06 10:03:57 +02:00
Родитель 331fcbd909
Коммит ae338e97c4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CCE73CEF3035D3C8
12 изменённых файлов: 267 добавлений и 84 удалений

Просмотреть файл

@ -67,7 +67,7 @@ return [
['name' => 'poll#toggleArchive', 'url' => '/poll/{pollId}/toggleArchive', 'verb' => 'PUT'],
['name' => 'poll#clone', 'url' => '/poll/{pollId}/clone', 'verb' => 'GET'],
['name' => 'poll#getParticipantsEmailAddresses', 'url' => '/poll/{pollId}/addresses', 'verb' => 'GET'],
['name' => 'poll#sendConfirmation', 'url' => '/poll/{pollId}/confirmation', 'verb' => 'POST'],
['name' => 'poll#transfer_polls', 'url' => '/polls/transfer/{sourceUser}/{destinationUser}', 'verb' => 'PUT'],
['name' => 'option#list', 'url' => '/poll/{pollId}/options', 'verb' => 'GET'],

Просмотреть файл

@ -30,6 +30,7 @@ use OCA\Polls\Service\PollService;
use OCA\Polls\Service\OptionService;
use OCA\Polls\Model\Acl;
use OCA\Polls\Model\Settings\AppSettings;
use OCA\Polls\Service\MailService;
use OCP\ISession;
class PollController extends BaseController {
@ -37,6 +38,9 @@ class PollController extends BaseController {
/** @var Acl */
private $acl;
/** @var MailService */
private $mailService;
/** @var OptionService */
private $optionService;
@ -45,14 +49,16 @@ class PollController extends BaseController {
public function __construct(
string $appName,
Acl $acl,
IRequest $request,
ISession $session,
Acl $acl,
MailService $mailService,
OptionService $optionService,
PollService $pollService
) {
parent::__construct($appName, $request, $session);
$this->acl = $acl;
$this->mailService = $mailService;
$this->optionService = $optionService;
$this->pollService = $pollService;
}
@ -104,6 +110,17 @@ class PollController extends BaseController {
]);
}
/**
* Send confirmation mails
* @NoAdminRequired
*/
public function sendConfirmation(int $pollId): JSONResponse {
$this->acl->setPollId($pollId, Acl::PERMISSION_POLL_EDIT);
return $this->response(fn () => [
'confirmations' => $this->mailService->sendConfirmation($pollId),
]);
}
/**
* Switch deleted status (move to deleted polls)
* @NoAdminRequired

Просмотреть файл

@ -57,7 +57,24 @@ class OptionMapper extends QBMapper {
}
/**
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Option[]
* @psalm-return array<array-key, Option>
*/
public function findConfirmed(int $pollId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))
)->andWhere(
$qb->expr()->gt('confirmed', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))
);
return $this->findEntities($qb);
}
/**
* @return Option[]
* @psalm-return array<array-key, Option>
*/

Просмотреть файл

@ -0,0 +1,79 @@
<?php
/**
* @copyright Copyright (c) 2021 René Gieling <github@dartcafe.de>
*
* @author René Gieling <github@dartcafe.de>
*
* @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\Polls\Model\Mail;
use OCA\Polls\Db\OptionMapper;
use OCA\Polls\Helper\Container;
class ConfirmationMail extends MailBase {
private const TEMPLATE_CLASS = 'polls.Confirmation';
/** @var OptionMapper */
protected $optionMapper;
public function __construct(
string $recipientId,
int $pollId
) {
parent::__construct($recipientId, $pollId);
$this->optionMapper = Container::queryClass(OptionMapper::class);
}
protected function getSubject(): string {
return $this->l10n->t('Poll "%s" has been closed', $this->poll->getTitle());
}
protected function getFooter(): string {
return $this->l10n->t('This email is sent to you, to inform you about the result of a poll, you participated in. At least your name or your email address is recorded in this poll. If you want to get removed from this poll, contact the site administrator or the initiator of this poll, where the mail is sent from.');
}
protected function buildBody(): void {
$this->emailTemplate->addBodyText(str_replace(
['{owner}', '{title}'],
[$this->owner->getDisplayName(), $this->poll->getTitle()],
$this->l10n->t('{owner} wants to inform you about the final result of the poll "{title}"')
));
$confirmedOptions = $this->optionMapper->findConfirmed($this->poll->getId());
$countConfirmed = count($confirmedOptions);
$this->emailTemplate->addBodyText(
$this->l10n->n('Confirmed option:', 'Confirmed options:', $countConfirmed)
);
foreach ($confirmedOptions as $option) {
$this->emailTemplate->addBodyListItem(
$option->getPollOptionText()
);
}
$this->emailTemplate->addBodyText($this->getRichDescription(), $this->poll->getDescription());
$this->addButtonToPoll();
$this->emailTemplate->addBodyText($this->l10n->t('This link gives you personal access to the poll named above. Press the button above or copy the following link and add it in your browser\'s location bar:'));
$this->emailTemplate->addBodyText($this->url);
$this->emailTemplate->addBodyText($this->l10n->t('Do not share this link with other people, because it is connected to your votes.'));
}
}

Просмотреть файл

@ -65,9 +65,7 @@ class InvitationMail extends MailBase {
$this->emailTemplate->addBodyText($this->getRichDescription(), $this->poll->getDescription());
if ($this->getButtonText() && $this->url) {
$this->emailTemplate->addBodyButton($this->getButtonText(), $this->url);
}
$this->addButtonToPoll();
$this->emailTemplate->addBodyText($this->l10n->t('This link gives you personal access to the poll named above. Press the button above or copy the following link and add it in your browser\'s location bar:'));
$this->emailTemplate->addBodyText($this->url);

Просмотреть файл

@ -30,9 +30,8 @@ use OCA\Polls\Helper\Container;
use OCA\Polls\Model\Settings\AppSettings;
use OCA\Polls\Model\UserGroup\UserBase;
use OCA\Polls\Model\UserGroup\User;
use OCA\Polls\Service\UserService;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
@ -45,52 +44,52 @@ use Psr\Log\LoggerInterface;
abstract class MailBase {
private const TEMPLATE_CLASS = 'polls.Mail';
/** @var UserBase */
protected $recipient;
/** @var LoggerInterface */
protected $logger;
/** @var Poll */
protected $poll;
/** @var string|null */
protected $url = null;
/** @var string */
protected $footer;
/** @var IMailer */
protected $mailer;
/** @var IL10N */
protected $l10n;
/** @var IFactory */
protected $transFactory;
/** @var IUserManager */
private $userManager;
/** @var AppSettings */
protected $appSettings;
/** @var IEmailTemplate */
protected $emailTemplate;
/** @var AppSettings */
protected $appSettings;
/** @var string */
protected $footer;
/** @var IL10N */
protected $l10n;
/** @var LoggerInterface */
protected $logger;
/** @var IMailer */
protected $mailer;
/** @var User */
protected $owner;
/** @var Poll */
protected $poll;
/** @var UserBase */
protected $recipient;
/** @var IFactory */
protected $transFactory;
/** @var UserService */
protected $userService;
/** @var string|null */
protected $url = null;
public function __construct(
string $recipientId,
int $pollId,
string $url = null
) {
$this->userManager = Container::queryClass(IUserManager::class);
$this->appSettings = Container::queryClass(AppSettings::class);
$this->logger = Container::queryClass(LoggerInterface::class);
$this->mailer = Container::queryClass(IMailer::class);
$this->transFactory = Container::queryClass(IFactory::class);
$this->appSettings = Container::queryClass(AppSettings::class);
$this->userService = Container::queryClass(UserService::class);
$this->poll = $this->getPoll($pollId);
$this->recipient = $this->getUser($recipientId);
@ -172,6 +171,12 @@ abstract class MailBase {
return $this->l10n->t('Go to poll');
}
protected function addButtonToPoll(): void {
if ($this->getButtonText() && $this->url) {
$this->emailTemplate->addBodyButton($this->getButtonText(), $this->url);
}
}
protected function getFooter(): string {
return $this->l10n->t('This email is sent to you, because you subscribed to notifications of this poll. To opt out, visit the poll and remove your subscription.');
}
@ -197,12 +202,7 @@ abstract class MailBase {
}
protected function getUser(string $userId) : UserBase {
if ($this->userManager->get($userId) instanceof IUser) {
// return User object
return new User($userId);
}
// return UserBaseChild from share
return Container::findShare($this->poll->getId(), $userId)->getUserObject();
return $this->userService->evaluateUser($userId, $this->poll->getId());
}
protected function getRichDescription() : string {

Просмотреть файл

@ -68,7 +68,7 @@ class NotificationMail extends MailBase {
$this->emailTemplate->addBodyListItem($this->getComposedLogString($logItem, $displayName));
}
$this->emailTemplate->addBodyButton($this->getButtonText(), $this->url);
$this->addButtonToPoll();
}
private function evaluateDisplayName(Log $logItem) {

Просмотреть файл

@ -66,14 +66,14 @@ class ReminderMail extends MailBase {
}
protected function buildBody(): void {
$this->addBoddyText();
$this->emailTemplate->addBodyButton($this->getButtonText(), $this->url);
$this->addBodyText();
$this->addButtonToPoll();
$this->emailTemplate->addBodyText($this->l10n->t('This link gives you personal access to the poll named above. Press the button above or copy the following link and add it in your browser\'s location bar:'));
$this->emailTemplate->addBodyText($this->url);
$this->emailTemplate->addBodyText($this->l10n->t('Do not share this link with other people, because it is connected to your votes.'));
}
private function addBoddyText(): void {
private function addBodyText(): void {
$dtDeadline = new DateTime('now', $this->recipient->getTimeZone());
$dtDeadline->setTimestamp($this->deadline);
$deadlineText = (string) $this->l10n->l('datetime', $dtDeadline, ['width' => 'long']);

Просмотреть файл

@ -23,22 +23,20 @@
namespace OCA\Polls\Service;
use OCA\Polls\Db\SubscriptionMapper;
use OCA\Polls\Db\OptionMapper;
use OCA\Polls\Db\PollMapper;
use OCA\Polls\Db\Poll;
use OCA\Polls\Db\ShareMapper;
use OCA\Polls\Db\Share;
use OCA\Polls\Db\LogMapper;
use OCA\Polls\Db\Log;
use OCA\Polls\Db\LogMapper;
use OCA\Polls\Db\Poll;
use OCA\Polls\Db\PollMapper;
use OCA\Polls\Db\Share;
use OCA\Polls\Db\ShareMapper;
use OCA\Polls\Db\SubscriptionMapper;
use OCA\Polls\Exceptions\InvalidEmailAddress;
use OCA\Polls\Exceptions\NoDeadLineException;
use OCA\Polls\Model\UserGroup\User;
use OCA\Polls\Model\Mail\ConfirmationMail;
use OCA\Polls\Model\Mail\InvitationMail;
use OCA\Polls\Model\Mail\ReminderMail;
use OCA\Polls\Model\Mail\NotificationMail;
use OCP\IUser;
use OCP\IUserManager;
use OCA\Polls\Model\Mail\ReminderMail;
use OCA\Polls\Model\UserGroup\UserBase;
use Psr\Log\LoggerInterface;
class MailService {
@ -51,55 +49,41 @@ class MailService {
/** @var Log[] **/
private $logs;
/** @var IUserManager */
private $userManager;
/** @var OptionMapper */
private $optionMapper;
/** @var PollMapper */
private $pollMapper;
/** @var SubscriptionMapper */
private $subscriptionMapper;
/** @var ShareMapper */
private $shareMapper;
/** @var SubscriptionMapper */
private $subscriptionMapper;
/** @var UserService */
private $userService;
public function __construct(
IUserManager $userManager,
LoggerInterface $logger,
LogMapper $logMapper,
OptionMapper $optionMapper,
PollMapper $pollMapper,
ShareMapper $shareMapper,
SubscriptionMapper $subscriptionMapper
SubscriptionMapper $subscriptionMapper,
UserService $userService
) {
$this->logger = $logger;
$this->logMapper = $logMapper;
$this->optionMapper = $optionMapper;
$this->pollMapper = $pollMapper;
$this->shareMapper = $shareMapper;
$this->subscriptionMapper = $subscriptionMapper;
$this->userManager = $userManager;
$this->userService = $userService;
$this->logs = [];
}
public function resolveEmailAddress(int $pollId, string $userId): string {
if ($this->userManager->get($userId) instanceof IUser) {
$user = new User($userId);
return $user->getEmailAddressMasked();
$user = $this->userService->evaluateUser($userId, $pollId);
if ($user->getEmailAddress()) {
return $user->getEmailAddress();
}
// if $userId is no site user, eval via shares
try {
$share = $this->shareMapper->findByPollAndUser($pollId, $userId);
if ($share->getEmailAddress()) {
return $share->getEmailAddress();
}
} catch (\Exception $e) {
// catch silently
}
return '';
}
@ -178,6 +162,26 @@ class MailService {
}
}
public function sendConfirmation($pollId): array {
$sentMails = [];
$abortedMails = [];
$participants = $this->userService->getParticipants($pollId);
foreach ($participants as $participant) {
if ($this->sendConfirmationToParticipant($participant, $pollId)) {
$sentMails[] = $participant->getDisplayName();
} else {
$abortedMails[] = $participant->getDisplayName();
}
}
return [
'sent' => $sentMails,
'error' => $abortedMails
];
}
private function processSharesForAutoReminder(Poll $poll) {
$shares = $this->shareMapper->findByPollUnreminded($poll->getId());
foreach ($shares as $share) {
@ -191,6 +195,23 @@ class MailService {
}
}
private function sendConfirmationToParticipant(UserBase $participant, int $pollId) : bool {
$confirmation = new ConfirmationMail(
$participant->getId(),
$pollId
);
try {
$confirmation->send();
return true;
} catch (InvalidEmailAddress $e) {
$this->logger->warning('Invalid or no email address for confirmation: ' . json_encode($participant));
} catch (\Exception $e) {
$this->logger->error('Error sending confirmation to ' . json_encode($participant));
}
return false;
}
private function sendAutoReminderToRecipients(Share $share, Poll $poll) {
foreach ($share->getUserObject()->getMembers() as $recipient) {
$reminder = new ReminderMail(
@ -203,7 +224,7 @@ class MailService {
} catch (InvalidEmailAddress $e) {
$this->logger->warning('Invalid or no email address for reminder: ' . json_encode($share));
} catch (\Exception $e) {
$this->logger->error('Error sending Reminder to ' . json_encode($share));
$this->logger->error('Error sending reminder to ' . json_encode($share));
}
}
}

Просмотреть файл

@ -24,6 +24,7 @@
namespace OCA\Polls\Service;
use OCA\Polls\Db\ShareMapper;
use OCA\Polls\Db\VoteMapper;
use OCA\Polls\Exceptions\Exception;
use OCA\Polls\Exceptions\InvalidShareTypeException;
use OCA\Polls\Model\UserGroup\Admin;
@ -59,18 +60,23 @@ class UserService {
/** @var ISearch */
private $userSearch;
/** @var VoteMapper */
private $voteMapper;
public function __construct(
ISearch $userSearch,
ISession $session,
IUserSession $userSession,
IUserManager $userManager,
ShareMapper $shareMapper
ShareMapper $shareMapper,
VoteMapper $voteMapper
) {
$this->userSearch = $userSearch;
$this->session = $session;
$this->shareMapper = $shareMapper;
$this->userSession = $userSession;
$this->userManager = $userManager;
$this->voteMapper = $voteMapper;
}
/**
@ -113,6 +119,23 @@ class UserService {
}
}
/**
* Get participans of a poll as array of users
* @return Admin|Circle|Contact|ContactGroup|Email|GenericUser|Group|User
*/
public function getParticipants($pollId) : array {
$users = [];
$participants = $this->voteMapper->findParticipantsByPoll($pollId);
foreach ($participants as &$participant) {
$user = $this->evaluateUser($participant->getUserId(), $pollId);
if ($user) {
$users[] = $this->evaluateUser($participant->getUserId(), $pollId);
}
}
return $users;
}
/**
* Create user from share
* @return Admin|Circle|Contact|ContactGroup|Email|GenericUser|Group|User

Просмотреть файл

@ -29,6 +29,12 @@
</template>
{{ closed ? t('polls', 'Reopen poll'): t('polls', 'Close poll') }}
</VueButton>
<VueButton @click="sendConfirmation()">
<template #icon>
<OpenPollIcon v-if="closed" />
</template>
{{ t('polls', 'Send Confirmation') }}
</VueButton>
<CheckboxRadioSwitch v-show="!closed" :checked.sync="useExpire" type="switch">
{{ t('polls', 'Poll closing date') }}
</CheckboxRadioSwitch>
@ -38,6 +44,7 @@
<script>
import { mapState, mapGetters } from 'vuex'
import { showError, showSuccess } from '@nextcloud/dialogs'
import moment from '@nextcloud/moment'
import { Button as VueButton, DatetimePicker, CheckboxRadioSwitch } from '@nextcloud/vue'
import OpenPollIcon from 'vue-material-design-icons/LockOpenVariant.vue'
@ -120,6 +127,16 @@ export default {
this.$emit('change')
},
async sendConfirmation() {
const confirmations = await this.$store.dispatch('poll/sendConfirmation')
confirmations.sent.forEach((confirmation) => {
showSuccess(t('polls', `Confirmation sent to ${confirmation}`))
})
confirmations.error.forEach((confirmation) => {
showError(t('polls', `Confirmation could not be sent to ${confirmation}`))
})
},
},
}
</script>

Просмотреть файл

@ -247,6 +247,17 @@ const actions = {
}
},
async sendConfirmation(context, payload) {
const endPoint = `apps/polls/poll/${context.rootState.route.params.id}/confirmation`
try {
const response = await axios.post(generateUrl(endPoint))
console.log(response.data)
return response.data.confirmations
} catch (e) {
console.error('Error sending confirmation', { error: e.response }, { payload })
}
},
async delete(context, payload) {
const endPoint = `apps/polls/poll/${payload.pollId}`
try {