Skip to content

Commit

Permalink
pkp#9253 Add site-level announcements (main) (pkp#9260)
Browse files Browse the repository at this point in the history
* pkp#9253 Add site-level announcements

* pkp#9253 Fix type error in announcement helper function

* pkp#9253 Use shorter syntax to filter array

* pkp#9253 Use null to associate announcements with the site

* pkp#9253 Move shared announcements template into pkp-lib

* pkp#9253 Fix URL in announcement API responses

* pkp#9253 Add featured image to site announcements

* pkp#9253 Fix error when adding announcement

* pkp#9253 Fix unwanted file deletion when editing announcement

* pkp#9253 Add image to default announcement templates

* pkp#9253 Make context nullable in announcements install migration

* pkp#9253 Require User when handling announcement image upload

* Fix exception name

* Refactor to new Controllers

* pkp#9253 Fix duplicate method name after rebase

* pkp#9253 Fix announcements API handler after rebase

* pkp#9253 Check image validity when creating announcement
  • Loading branch information
NateWr authored and ipula committed Dec 4, 2023
1 parent 9995c2e commit cda6344
Show file tree
Hide file tree
Showing 26 changed files with 830 additions and 181 deletions.
172 changes: 112 additions & 60 deletions api/v1/announcements/PKPAnnouncementController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
use Illuminate\Support\Facades\Route;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\announcement\Collector;
use PKP\context\Context;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\jobs\notifications\NewAnnouncementNotifyUsers;
Expand Down Expand Up @@ -63,7 +66,6 @@ public function getRouteGroupMiddleware(): array
{
return [
'has.user',
'has.context',
self::roleAuthorizer([
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Expand Down Expand Up @@ -100,6 +102,10 @@ public function getGroupRoutes(): void
*/
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
{
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}

$this->addPolicy(new UserRolesRequiredPolicy($request), true);

$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
Expand Down Expand Up @@ -127,7 +133,7 @@ public function get(Request $illuminateRequest): JsonResponse
}

// The assocId in announcements should always point to the contextId
if ($announcement->getData('assocId') !== $this->getRequest()->getContext()->getId()) {
if ($announcement->getData('assocId') !== $this->getRequest()->getContext()?->getId()) {
return response()->json([
'error' => __('api.announcements.400.contextsNotMatched')
], Response::HTTP_BAD_REQUEST);
Expand Down Expand Up @@ -166,7 +172,12 @@ public function getMany(Request $illuminateRequest): JsonResponse
}
}

$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
if ($this->getRequest()->getContext()) {
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
} else {
$collector->withSiteAnnouncements(Collector::SITE_ONLY);
}


Hook::run('API::announcements::params', [$collector, $illuminateRequest]);

Expand All @@ -186,72 +197,40 @@ public function add(Request $illuminateRequest): JsonResponse
$request = $this->getRequest();
$context = $request->getContext();

if (!$context) {
throw new Exception('You can not add an announcement without sending a request to the API endpoint of a particular context.');
}

$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $illuminateRequest->input());
$params['assocType'] = Application::get()->getContextAssocType();
$params['assocId'] = $request->getContext()->getId();
$params['assocId'] = $context?->getId();

$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();
$errors = Repo::announcement()->validate(null, $params, $allowedLocales, $primaryLocale);

if (!empty($errors)) {
return response()->json($errors, Response::HTTP_BAD_REQUEST);
}

$announcement = Repo::announcement()->newDataObject($params);
$announcementId = Repo::announcement()->add($announcement);
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN);
$contextId = $context->getId();

/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');

// Notify users
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$contextId]
);

if ($sendEmail) {
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$contextId]
);

$userIdsToNotifyAndMail = $userIdsToNotify->intersect($userIdsToMail);
$userIdsToNotify = $userIdsToNotify->diff($userIdsToMail);
try {
$announcementId = Repo::announcement()->add($announcement);
} catch (StoreTemporaryFileException $e) {
$announcementId = $e->dataObject->getId();
if ($announcementId) {
$announcement = Repo::announcement()->get($announcementId);
Repo::announcement()->delete($announcement);
}
return response()->json([
'image' => [__('api.400.errorUploadingImage')]
], Response::HTTP_BAD_REQUEST);
}

$sender = $request->getUser();
$jobs = [];
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyUserIds,
$contextId,
$announcementId,
Locale::getPrimaryLocale()
);
}
$announcement = Repo::announcement()->get($announcementId);

if (isset($userIdsToNotifyAndMail)) {
foreach ($userIdsToNotifyAndMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $notifyAndMailUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyAndMailUserIds,
$contextId,
$announcementId,
Locale::getPrimaryLocale(),
$sender
);
}
}
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN);

Bus::batch($jobs)->dispatch();
if ($context) {
$this->notifyUsers($request, $context, $announcementId, $sendEmail);
}

return response()->json(Repo::announcement()->getSchemaMap()->map($announcement), Response::HTTP_OK);
}
Expand All @@ -262,6 +241,7 @@ public function add(Request $illuminateRequest): JsonResponse
public function edit(Request $illuminateRequest): JsonResponse
{
$request = $this->getRequest();
$context = $request->getContext();

$announcement = Repo::announcement()->get((int) $illuminateRequest->route('announcementId'));

Expand All @@ -276,7 +256,7 @@ public function edit(Request $illuminateRequest): JsonResponse
}

// Don't allow to edit an announcement from one context from a different context's endpoint
if ($request->getContext()->getId() !== $announcement->getData('assocId')) {
if ($request->getContext()?->getId() !== $announcement->getData('assocId')) {
return response()->json([
'error' => __('api.announcements.400.contextsNotMatched')
], Response::HTTP_FORBIDDEN);
Expand All @@ -286,16 +266,22 @@ public function edit(Request $illuminateRequest): JsonResponse
$params['id'] = $announcement->getId();
$params['typeId'] ??= null;

$context = $request->getContext();
$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();

$errors = Repo::announcement()->validate($announcement, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return response()->json($errors, Response::HTTP_BAD_REQUEST);
}

Repo::announcement()->edit($announcement, $params);
try {
Repo::announcement()->edit($announcement, $params);
} catch (StoreTemporaryFileException $e) {
Repo::announcement()->delete($announcement);
return response()->json([
'image' => [__('api.400.errorUploadingImage')]
], Response::HTTP_BAD_REQUEST);
}

$announcement = Repo::announcement()->get($announcement->getId());

Expand All @@ -322,7 +308,7 @@ public function delete(Request $illuminateRequest): JsonResponse
}

// Don't allow to delete an announcement from one context from a different context's endpoint
if ($request->getContext()->getId() !== $announcement->getData('assocId')) {
if ($request->getContext()?->getId() !== $announcement->getData('assocId')) {
return response()->json([
'error' => __('api.announcements.400.contextsNotMatched')
], Response::HTTP_FORBIDDEN);
Expand All @@ -334,4 +320,70 @@ public function delete(Request $illuminateRequest): JsonResponse

return response()->json($announcementProps, Response::HTTP_OK);
}

/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
return array_filter($roleAssignments, fn($key) => $key == Role::ROLE_ID_SITE_ADMIN, ARRAY_FILTER_USE_KEY);
}

/**
* Notify subscribed users
*
* This only works for context-level announcements. There is no way to
* determine users who have subscribed to site-level announcements.
*
* @param bool $sendEmail Whether or not the editor chose to notify users by email
*/
protected function notifyUsers(PKPRequest $request, Context $context, int $announcementId, bool $sendEmail): void
{
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');

// Notify users
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);

if ($sendEmail) {
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);

$userIdsToNotifyAndMail = $userIdsToNotify->intersect($userIdsToMail);
$userIdsToNotify = $userIdsToNotify->diff($userIdsToMail);
}

$sender = $request->getUser();
$jobs = [];
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale()
);
}

if (isset($userIdsToNotifyAndMail)) {
foreach ($userIdsToNotifyAndMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $notifyAndMailUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyAndMailUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale(),
$sender
);
}
}

Bus::batch($jobs)->dispatch();
}
}
64 changes: 64 additions & 0 deletions classes/announcement/Announcement.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@

namespace PKP\announcement;

use APP\core\Application;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use PKP\db\DAORegistry;

class Announcement extends \PKP\core\DataObject
Expand Down Expand Up @@ -275,6 +278,67 @@ public function setDatetimePosted($datetimePosted)
{
$this->setData('datePosted', $datetimePosted);
}

/**
* Get the featured image data
*/
public function getImage(): ?array
{
return $this->getData('image');
}

/**
* Set the featured image data
*/
public function setImage(array $image): void
{
$this->setData('image', $image);
}

/**
* Get the full URL to the image
*
* @param bool $withTimestamp Pass true to include a query argument with a timestamp
* of the date the image was uploaded in order to workaround cache bugs in browsers
*/
public function getImageUrl(bool $withTimestamp = true): string
{
$image = $this->getImage();

if (!$image) {
return '';
}

$filename = $image['uploadName'];
if ($withTimestamp) {
$filename .= '?'. strtotime($image['dateUploaded']);
}

$publicFileManager = new PublicFileManager();

return join('/', [
Application::get()->getRequest()->getBaseUrl(),
$this->getAssocId()
? $publicFileManager->getContextFilesPath((int) $this->getAssocId())
: $publicFileManager->getSiteFilesPath(),
Repo::announcement()->getImageSubdirectory(),
$filename
]);
}

/**
* Get the alt text for the image
*/
public function getImageAltText(): string
{
$image = $this->getImage();

if (!$image || !$image['altText']) {
return '';
}

return $image['altText'];
}
}

if (!PKP_STRICT_MODE) {
Expand Down
Loading

0 comments on commit cda6344

Please sign in to comment.