Skip to content

Commit

Permalink
#9253 Add site-level announcements
Browse files Browse the repository at this point in the history
  • Loading branch information
NateWr authored and asmecher committed Nov 8, 2023
1 parent c1f26ec commit 4835308
Show file tree
Hide file tree
Showing 15 changed files with 442 additions and 125 deletions.
157 changes: 100 additions & 57 deletions api/v1/announcements/PKPAnnouncementHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
namespace PKP\API\v1\announcements;

use APP\core\Application;
use APP\core\Request;
use APP\facades\Repo;
use Exception;
use Illuminate\Support\Facades\Bus;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
Expand Down Expand Up @@ -92,6 +94,10 @@ public function __construct()
*/
public function authorize($request, &$args, $roleAssignments)
{
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}

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

$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
Expand Down Expand Up @@ -163,7 +169,7 @@ public function getMany($slimRequest, $response, $args)
}
}

$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
$collector->filterByContextIds([$this->getContextId()]);

Hook::call('API::submissions::params', [$collector, $slimRequest]);

Expand All @@ -188,17 +194,14 @@ public function add($slimRequest, $response, $args)
{
$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.');
}
$contextId = $this->getContextId();

$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $slimRequest->getParsedBody());
$params['assocType'] = Application::get()->getContextAssocType();
$params['assocId'] = $request->getContext()->getId();
$params['assocId'] = $contextId;

$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)) {
Expand All @@ -208,54 +211,11 @@ public function add($slimRequest, $response, $args)
$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);
}

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

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

Bus::batch($jobs)->dispatch();

return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}

Expand Down Expand Up @@ -283,7 +243,7 @@ public function edit($slimRequest, $response, $args)
}

// Don't allow to edit an announcement from one context from a different context's endpoint
if ($request->getContext()->getId() !== $announcement->getData('assocId')) {
if ($this->getContextId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}

Expand All @@ -292,8 +252,8 @@ public function edit($slimRequest, $response, $args)
$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)) {
Expand Down Expand Up @@ -331,7 +291,7 @@ public function delete($slimRequest, $response, $args)
}

// Don't allow to delete an announcement from one context from a different context's endpoint
if ($request->getContext()->getId() !== $announcement->getData('assocId')) {
if ($this->getContextId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}

Expand All @@ -341,4 +301,87 @@ public function delete($slimRequest, $response, $args)

return $response->withJson($announcementProps, 200);
}

/**
* Get the context id or site-wide context id of the current request
*/
protected function getContextId(): int
{
$context = $this->getRequest()->getContext();
return $context
? $context->getId()
: Application::CONTEXT_ID_NONE;
}

/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
$roleIds = array_keys($roleAssignments);
foreach ($roleIds as $roleId) {
if ($roleId !== Role::ROLE_ID_SITE_ADMIN) {
unset($roleAssignments[$roleId]);
}
}
return $roleAssignments;
}

/**
* 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(Request $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();
}
}
31 changes: 31 additions & 0 deletions classes/announcement/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@
*/
class Collector implements CollectorInterface
{
public const ORDERBY_DATE_POSTED = 'date_posted';
public const ORDERBY_DATE_EXPIRE = 'date_expire';
public const ORDER_DIR_ASC = 'ASC';
public const ORDER_DIR_DESC = 'DESC';

public DAO $dao;
public ?array $contextIds = null;
public ?string $isActive = null;
public ?string $searchPhrase = null;
public ?array $typeIds = null;
public ?int $count = null;
public ?int $offset = null;
public string $orderBy = self::ORDERBY_DATE_POSTED;
public string $orderDirection = self::ORDER_DIR_DESC;

public function __construct(DAO $dao)
{
Expand Down Expand Up @@ -126,6 +133,21 @@ public function offset(?int $offset): self
return $this;
}

/**
* Order the results
*
* Results are ordered by the date posted by default.
*
* @param string $sorter One of the self::ORDERBY_ constants
* @param string $direction One of the self::ORDER_DIR_ constants
*/
public function orderBy(?string $sorter, string $direction = self::ORDER_DIR_DESC): self
{
$this->orderBy = $sorter;
$this->orderDirection = $direction;
return $this;
}

/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
Expand Down Expand Up @@ -184,6 +206,15 @@ public function getQueryBuilder(): Builder
$qb->offset($this->offset);
}

if (isset($this->orderBy)) {
$qb->orderBy('a.' . $this->orderBy, $this->orderDirection);
// Add a secondary sort by id to catch cases where two
// announcements share the same date
if (in_array($this->orderBy, [SELF::ORDERBY_DATE_EXPIRE, SELF::ORDERBY_DATE_POSTED])) {
$qb->orderBy('a.announcement_id', $this->orderDirection);
}
}

Hook::call('Announcement::Collector', [&$qb, $this]);

return $qb;
Expand Down
28 changes: 26 additions & 2 deletions classes/announcement/maps/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace PKP\announcement\maps;

use APP\core\Application;
use APP\core\Request;
use Illuminate\Support\Enumerable;
use PKP\announcement\Announcement;
use PKP\core\PKPApplication;
Expand All @@ -24,6 +26,12 @@ class Schema extends \PKP\core\maps\Schema

public string $schema = PKPSchemaService::SCHEMA_ANNOUNCEMENT;

public function __construct(Request $request, PKPSchemaService $schemaService)
{
$this->request = $request;
$this->schemaService = $schemaService;
}

/**
* Map an announcement
*
Expand Down Expand Up @@ -85,7 +93,7 @@ protected function mapByProperties(array $props, Announcement $item): array
$output[$prop] = $this->request->getDispatcher()->url(
$this->request,
PKPApplication::ROUTE_PAGE,
$this->context->getData('urlPath'),
$this->getUrlPath(),
'announcement',
'view',
$item->getId()
Expand All @@ -97,10 +105,26 @@ protected function mapByProperties(array $props, Announcement $item): array
}
}

$output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedFormLocales());
$output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->getSupportedLocales());

ksort($output);

return $this->withExtensions($output, $item);
}

protected function getUrlPath(): string
{
if (isset($this->context)) {
return $this->context->getData('urlPath');
}
return 'index';
}

protected function getSupportedLocales(): array
{
if (isset($this->context)) {
return $this->context->getSupportedFormLocales();
}
return Application::get()->getRequest()->getSite()->getSupportedLocales();
}
}
Loading

0 comments on commit 4835308

Please sign in to comment.