diff --git a/api/v1/invitations/InvitationController.php b/api/v1/invitations/InvitationController.php
index 5176e332c14..0d1ea50d1db 100644
--- a/api/v1/invitations/InvitationController.php
+++ b/api/v1/invitations/InvitationController.php
@@ -221,6 +221,14 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme
throw new Exception('This invitation does not support API handling');
}
+ if (in_array($actionName, $this->requiresOnlyId) && $this->invitation->getStatus() != InvitationStatus::INITIALIZED) {
+ throw new Exception('This action is not allowed');
+ }
+
+ if (in_array($actionName, $this->requiresIdAndKey) && $this->invitation->getStatus() != InvitationStatus::PENDING) {
+ throw new Exception('This action is not allowed');
+ }
+
$this->createInvitationHandler = $invitation->getCreateInvitationController($this->invitation);
$this->receiveInvitationHandler = $invitation->getReceiveInvitationController($this->invitation);
diff --git a/classes/components/forms/context/PKPAppearanceSetupForm.php b/classes/components/forms/context/PKPAppearanceSetupForm.php
index 84c05c16c7f..684d037c1c2 100644
--- a/classes/components/forms/context/PKPAppearanceSetupForm.php
+++ b/classes/components/forms/context/PKPAppearanceSetupForm.php
@@ -43,33 +43,18 @@ public function __construct($action, $locales, $context, $baseUrl, $temporaryFil
$this->action = $action;
$this->locales = $locales;
$sidebarOptions = [];
- $enabledOptions = [];
- $disabledOptions = [];
$currentBlocks = (array) $context->getData('sidebar');
$plugins = PluginRegistry::loadCategory('blocks', true);
- foreach ($currentBlocks as $plugin) {
- if (isset($plugins[$plugin])) {
- $enabledOptions[] = [
- 'value' => $plugin,
- 'label' => htmlspecialchars($plugins[$plugin]->getDisplayName()),
- ];
- }
- }
-
foreach ($plugins as $pluginName => $plugin) {
- if (!in_array($pluginName, $currentBlocks)) {
- $disabledOptions[] = [
- 'value' => $pluginName,
- 'label' => htmlspecialchars($plugin->getDisplayName()),
- ];
- }
+ $sidebarOptions[] = [
+ 'value' => $pluginName,
+ 'label' => htmlspecialchars($plugin->getDisplayName()),
+ ];
}
- $sidebarOptions = array_merge($enabledOptions, $disabledOptions);
-
$this->addField(new FieldUploadImage('pageHeaderLogoImage', [
'label' => __('manager.setup.logo'),
'value' => $context->getData('pageHeaderLogoImage'),
diff --git a/classes/components/forms/invitation/AcceptUserDetailsForm.php b/classes/components/forms/invitation/AcceptUserDetailsForm.php
new file mode 100644
index 00000000000..19e73b0dcae
--- /dev/null
+++ b/classes/components/forms/invitation/AcceptUserDetailsForm.php
@@ -0,0 +1,87 @@
+action = $action;
+ $this->locales = $locales;
+
+ $countries = [];
+ foreach (Locale::getCountries() as $country) {
+ $countries[] = [
+ 'value' => $country->getAlpha2(),
+ 'label' => $country->getLocalName()
+ ];
+ }
+
+ usort($countries, function ($a, $b) {
+ return strcmp($a['label'], $b['label']);
+ });
+
+ $this->addField(new FieldText('givenName', [
+ 'label' => __('user.givenName'),
+ 'description' => __('acceptInvitation.userDetailsForm.givenName.description'),
+ 'isRequired' => true,
+ 'isMultilingual' => true,
+ 'size' => 'large',
+ 'value' => ''
+ ]))
+ ->addField(new FieldText('familyName', [
+ 'label' => __('user.familyName'),
+ 'description' => __('acceptInvitation.userDetailsForm.familyName.description'),
+ 'isRequired' => false,
+ 'isMultilingual' => true,
+ 'size' => 'large',
+ 'value' => ''
+ ]))
+ ->addField(new FieldText('affiliation', [
+ 'label' => __('user.affiliation'),
+ 'description' => __('acceptInvitation.userDetailsForm.affiliation.description'),
+ 'isMultilingual' => true,
+ 'isRequired' => false,
+ 'size' => 'large',
+
+ ]))
+ ->addField(new FieldSelect('userCountry', [
+ 'label' => __('acceptInvitation.userDetailsForm.countryOfAffiliation.label'),
+ 'description' => __('acceptInvitation.userDetailsForm.countryOfAffiliation.description'),
+ 'options' => $countries,
+ 'isRequired' => true,
+ 'size' => 'large',
+ ]));
+
+ }
+}
diff --git a/classes/components/forms/invitation/UserDetailsForm.php b/classes/components/forms/invitation/UserDetailsForm.php
new file mode 100644
index 00000000000..c892e0a7c32
--- /dev/null
+++ b/classes/components/forms/invitation/UserDetailsForm.php
@@ -0,0 +1,68 @@
+action = $action;
+ $this->locales = $locales;
+
+ $this->addField(new FieldText('inviteeEmail', [
+ 'label' => __('user.email'),
+ 'description' => __('invitation.email.description'),
+ 'isRequired' => true,
+ 'size' => 'large',
+ ]))
+ ->addField(new FieldHTML('orcid', [
+ 'label' => __('user.orcid'),
+ 'description' => __('invitation.orcid.description'),
+ 'isRequired' => false,
+ 'size' => 'large',
+ ]))
+ ->addField(new FieldText('givenName', [
+ 'label' => __('user.givenName'),
+ 'description' => __('invitation.givenName.description'),
+ 'isRequired' => false,
+ 'isMultilingual' => true,
+ 'size' => 'large',
+ ]))
+ ->addField(new FieldText('familyName', [
+ 'label' => __('user.familyName'),
+ 'description' => __('invitation.familyName.description'),
+ 'isRequired' => false,
+ 'isMultilingual' => true,
+ 'size' => 'large',
+ ]));
+ }
+}
diff --git a/classes/core/Dispatcher.php b/classes/core/Dispatcher.php
index 5d03720f640..1e8d3455671 100644
--- a/classes/core/Dispatcher.php
+++ b/classes/core/Dispatcher.php
@@ -304,10 +304,10 @@ public function _cacheContent(string $contents): string
/**
* Handle a 404 error (page not found).
*/
- public static function handle404()
+ public static function handle404(string $message = "404 Not Found"): void
{
header('HTTP/1.0 404 Not Found');
- echo "
404 Not Found
\n";
+ echo "" . htmlspecialchars($message) . "
\n";
exit;
}
}
diff --git a/classes/file/PKPLibraryFileManager.php b/classes/file/PKPLibraryFileManager.php
index 877c945f982..78cede096bc 100644
--- a/classes/file/PKPLibraryFileManager.php
+++ b/classes/file/PKPLibraryFileManager.php
@@ -20,6 +20,7 @@
use PKP\context\LibraryFile;
use PKP\context\LibraryFileDAO;
use PKP\db\DAORegistry;
+use PKP\plugins\Hook;
class PKPLibraryFileManager extends PrivateFileManager
{
@@ -168,6 +169,7 @@ public function getFileSuffixFromType($type)
/**
* Get the type => suffix mapping array
*
+ * @hook PublisherLibrary::types::suffixes [[&$map]]
* @return array
*/
public function &getTypeSuffixMap()
@@ -178,6 +180,7 @@ public function &getTypeSuffixMap()
LibraryFile::LIBRARY_FILE_TYPE_REPORT => 'REP',
LibraryFile::LIBRARY_FILE_TYPE_OTHER => 'OTH'
];
+ Hook::call('PublisherLibrary::types::suffixes', [&$map]);
return $map;
}
@@ -199,6 +202,7 @@ public function getNameFromType($type)
/**
* Get the type => locale key mapping array
*
+ * @hook PublisherLibrary::types::titles [[&$map]]
* @return array
*/
public function &getTypeTitleKeyMap()
@@ -209,6 +213,7 @@ public function &getTypeTitleKeyMap()
LibraryFile::LIBRARY_FILE_TYPE_REPORT => 'settings.libraryFiles.category.reports',
LibraryFile::LIBRARY_FILE_TYPE_OTHER => 'settings.libraryFiles.category.other'
];
+ Hook::call('PublisherLibrary::types::titles', [&$map]);
return $map;
}
@@ -226,6 +231,7 @@ public function getTitleKeyFromType($type)
/**
* Get the type => name mapping array
*
+ * @hook PublisherLibrary::types::names [[&$typeNameMap]]
* @return array
*/
public function &getTypeNameMap()
@@ -236,6 +242,7 @@ public function &getTypeNameMap()
LibraryFile::LIBRARY_FILE_TYPE_REPORT => 'reports',
LibraryFile::LIBRARY_FILE_TYPE_OTHER => 'other',
];
+ Hook::call('PublisherLibrary::types::names', [&$typeNameMap]);
return $typeNameMap;
}
}
diff --git a/classes/invitation/invitations/userRoleAssignment/UserRoleAssignmentInvite.php b/classes/invitation/invitations/userRoleAssignment/UserRoleAssignmentInvite.php
index eb79e1b62d0..e3dafeb0e5d 100644
--- a/classes/invitation/invitations/userRoleAssignment/UserRoleAssignmentInvite.php
+++ b/classes/invitation/invitations/userRoleAssignment/UserRoleAssignmentInvite.php
@@ -45,7 +45,6 @@ class UserRoleAssignmentInvite extends Invitation implements IApiHandleable
protected array $notAccessibleAfterInvite = [
'userGroupsToAdd',
- 'userGroupsToRemove',
];
protected array $notAccessibleBeforeInvite = [
@@ -164,8 +163,7 @@ public function getValidationRules(ValidationContext $validationContext = Valida
$validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE
) {
$invitationValidationRules[Invitation::VALIDATION_RULE_GENERIC][] = new NoUserGroupChangesRule(
- $this->getPayload()->userGroupsToAdd,
- $this->getPayload()->userGroupsToRemove
+ $this->getPayload()->userGroupsToAdd
);
$invitationValidationRules[Invitation::VALIDATION_RULE_GENERIC][] = new UserMustExistRule($this->getUserId());
$invitationValidationRules[Invitation::VALIDATION_RULE_GENERIC][] = new EmailMustNotExistRule($this->getEmail());
diff --git a/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php b/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php
index c0832538780..6fa237e5fb9 100644
--- a/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php
+++ b/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php
@@ -15,9 +15,13 @@
use APP\core\Request;
use APP\template\TemplateManager;
+use PKP\core\PKPApplication;
use PKP\invitation\core\enums\InvitationAction;
+use PKP\invitation\core\enums\InvitationStatus;
use PKP\invitation\core\InvitationActionRedirectController;
use PKP\invitation\invitations\userRoleAssignment\UserRoleAssignmentInvite;
+use PKP\invitation\stepTypes\AcceptInvitationStep;
+use APP\facades\Repo;
class UserRoleAssignmentInviteRedirectController extends InvitationActionRedirectController
{
@@ -26,17 +30,62 @@ public function getInvitation(): UserRoleAssignmentInvite
return $this->invitation;
}
+ /**
+ * Redirect to accept invitation page
+ * @param Request $request
+ * @return void
+ * @throws \Exception
+ */
public function acceptHandle(Request $request): void
{
$templateMgr = TemplateManager::getManager($request);
-
$templateMgr->assign('invitation', $this->invitation);
- $templateMgr->display('frontend/pages/invitations.tpl');
+ $context = $request->getContext();
+ $steps = new AcceptInvitationStep();
+ $invitationModel = $this->invitation->invitationModel->toArray();
+ $user = $invitationModel['userId'] ?Repo::user()->get($invitationModel['userId']) : null;
+ $templateMgr->setState([
+ 'steps' => $steps->getSteps($this->invitation,$context,$user),
+ 'primaryLocale' => $context->getData('primaryLocale'),
+ 'pageTitle' => __('invitation.wizard.pageTitle'),
+ 'invitationId' => (int)$request->getUserVar('id') ?: null,
+ 'invitationKey' => $request->getUserVar('key') ?: null,
+ 'pageTitleDescription' => __('invitation.wizard.pageTitleDescription'),
+ ]);
+ $templateMgr->assign([
+ 'pageComponent' => 'PageOJS',
+ ]);
+ $templateMgr->display('invitation/acceptInvitation.tpl');
}
+ /**
+ * Redirect to login page after decline invitation
+ * @param Request $request
+ * @return void
+ * @throws \Exception
+ */
public function declineHandle(Request $request): void
{
- return;
+ if ($this->invitation->getStatus() !== InvitationStatus::PENDING) {
+ $request->getDispatcher()->handle404('The link is deactivated as the invitation was cancelled');
+ }
+
+ $context = $request->getContext();
+
+ $url = PKPApplication::get()->getDispatcher()->url(
+ PKPApplication::get()->getRequest(),
+ PKPApplication::ROUTE_PAGE,
+ $context->getData('urlPath'),
+ 'login',
+ null,
+ null,
+ [
+ ]
+ );
+
+ $this->getInvitation()->decline();
+
+ $request->redirectUrl($url);
}
public function preRedirectActions(InvitationAction $action)
diff --git a/classes/invitation/invitations/userRoleAssignment/handlers/api/UserRoleAssignmentReceiveController.php b/classes/invitation/invitations/userRoleAssignment/handlers/api/UserRoleAssignmentReceiveController.php
index dfaf39094ef..fc8506357bb 100644
--- a/classes/invitation/invitations/userRoleAssignment/handlers/api/UserRoleAssignmentReceiveController.php
+++ b/classes/invitation/invitations/userRoleAssignment/handlers/api/UserRoleAssignmentReceiveController.php
@@ -15,6 +15,7 @@
namespace PKP\invitation\invitations\userRoleAssignment\handlers\api;
use APP\facades\Repo;
+use Carbon\Carbon;
use Core;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -110,22 +111,19 @@ public function finalize(Request $illuminateRequest): JsonResponse
}
}
- foreach ($this->invitation->getPayload()->userGroupsToRemove as $userUserGroup) {
- $userGroupHelper = UserGroupHelper::fromArray($userUserGroup);
- Repo::userGroup()->endAssignments(
- $this->invitation->getContextId(),
- $user->getId(),
- $userGroupHelper->userGroupId
- );
- }
-
foreach ($this->invitation->getPayload()->userGroupsToAdd as $userUserGroup) {
$userGroupHelper = UserGroupHelper::fromArray($userUserGroup);
+ $dateStart = Carbon::parse($userGroupHelper->dateStart)->startOfDay();
+ $today = Carbon::today();
+
+ // Use today's date if dateStart is in the past, otherwise keep dateStart
+ $effectiveDateStart = $dateStart->lessThan($today) ? $today->toDateString() : $dateStart->toDateString();
+
Repo::userGroup()->assignUserToGroup(
$user->getId(),
$userGroupHelper->userGroupId,
- $userGroupHelper->dateStart,
+ $effectiveDateStart,
$userGroupHelper->dateEnd,
isset($userGroupHelper->masthead) && $userGroupHelper->masthead
? UserUserGroupMastheadStatus::STATUS_ON
diff --git a/classes/invitation/invitations/userRoleAssignment/payload/UserRoleAssignmentInvitePayload.php b/classes/invitation/invitations/userRoleAssignment/payload/UserRoleAssignmentInvitePayload.php
index c4dc6cebcdd..38b685ad79c 100644
--- a/classes/invitation/invitations/userRoleAssignment/payload/UserRoleAssignmentInvitePayload.php
+++ b/classes/invitation/invitations/userRoleAssignment/payload/UserRoleAssignmentInvitePayload.php
@@ -14,14 +14,15 @@
namespace PKP\invitation\invitations\userRoleAssignment\payload;
+use DAORegistry;
use Illuminate\Validation\Rule;
use PKP\invitation\core\enums\ValidationContext;
use PKP\invitation\core\InvitePayload;
use PKP\invitation\invitations\userRoleAssignment\rules\AddUserGroupRule;
use PKP\invitation\invitations\userRoleAssignment\rules\AllowedKeysRule;
use PKP\invitation\invitations\userRoleAssignment\rules\NotNullIfPresent;
+use PKP\invitation\invitations\userRoleAssignment\rules\PrimaryLocaleRequired;
use PKP\invitation\invitations\userRoleAssignment\rules\ProhibitedIncludingNull;
-use PKP\invitation\invitations\userRoleAssignment\rules\RemoveUserGroupRule;
use PKP\invitation\invitations\userRoleAssignment\rules\UserGroupExistsRule;
use PKP\invitation\invitations\userRoleAssignment\rules\UsernameExistsRule;
use PKP\invitation\invitations\userRoleAssignment\UserRoleAssignmentInvite;
@@ -39,20 +40,21 @@ public function __construct(
public ?string $emailSubject = null,
public ?string $emailBody = null,
public ?array $userGroupsToAdd = null,
- public ?array $userGroupsToRemove = null,
public ?bool $passwordHashed = null,
public ?string $sendEmailAddress = null,
)
{
parent::__construct(get_object_vars($this));
-
-
}
public function getValidationRules(UserRoleAssignmentInvite $invitation, ValidationContext $validationContext = ValidationContext::VALIDATION_CONTEXT_DEFAULT): array
{
$context = $invitation->getContext();
$allowedLocales = $context->getSupportedFormLocales();
+ $primaryLocale = $context->getPrimaryLocale();
+
+ $siteDao = DAORegistry::getDAO('SiteDAO');
+ $site = $siteDao->getSite();
$validationRules = [
'givenName' => [
@@ -63,23 +65,29 @@ public function getValidationRules(UserRoleAssignmentInvite $invitation, Validat
Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
'sometimes',
'array',
- new AllowedKeysRule($allowedLocales), // Apply the custom rule
+ new AllowedKeysRule($allowedLocales),
],
'givenName.*' => [
+ 'nullable', // Make optional for other locales
'string',
'max:255',
],
+ "givenName.{$primaryLocale}" => [
+ 'sometimes',
+ Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
+ Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_REFINE),
+ ],
'familyName' => [
'bail',
Rule::excludeIf(!is_null($invitation->getUserId()) && $validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
new ProhibitedIncludingNull(!is_null($invitation->getUserId())),
- Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE]), ['nullable']),
- Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
+ Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE, ValidationContext::VALIDATION_CONTEXT_FINALIZE]), ['nullable']),
'sometimes',
'array',
- new AllowedKeysRule($allowedLocales), // Apply the custom rule
+ new AllowedKeysRule($allowedLocales),
],
'familyName.*' => [
+ 'nullable', // Make optional for all locales
'string',
'max:255',
],
@@ -87,13 +95,13 @@ public function getValidationRules(UserRoleAssignmentInvite $invitation, Validat
'bail',
Rule::excludeIf(!is_null($invitation->getUserId()) && $validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
new ProhibitedIncludingNull(!is_null($invitation->getUserId())),
- Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE]), ['nullable']),
- Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
+ Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE, ValidationContext::VALIDATION_CONTEXT_FINALIZE]), ['nullable']),
'sometimes',
'array',
- new AllowedKeysRule($allowedLocales), // Apply the custom rule
+ new AllowedKeysRule($allowedLocales),
],
'affiliation.*' => [
+ 'nullable', // Make optional for all locales
'string',
'max:255',
],
@@ -103,6 +111,8 @@ public function getValidationRules(UserRoleAssignmentInvite $invitation, Validat
new ProhibitedIncludingNull(!is_null($invitation->getUserId())),
Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE]), ['nullable']),
Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_FINALIZE),
+ 'sometimes', // Applies the rule only if userCountry exists in the request
+ Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_REFINE),
'string',
'max:255',
],
@@ -126,6 +136,7 @@ public function getValidationRules(UserRoleAssignmentInvite $invitation, Validat
new NotNullIfPresent(),
'required_with:username',
'max:255',
+ 'min:' . $site->getMinPasswordLength(),
],
'userGroupsToAdd' => [
Rule::requiredIf($validationContext === ValidationContext::VALIDATION_CONTEXT_INVITE),
@@ -145,24 +156,6 @@ public function getValidationRules(UserRoleAssignmentInvite $invitation, Validat
new AddUserGroupRule($invitation),
],
'userGroupsToAdd.*.masthead' => 'required|bool',
- 'userGroupsToAdd.*.dateStart' => 'required|date|after_or_equal:today',
- 'userGroupsToRemove' => [
- 'sometimes',
- 'bail',
- new ProhibitedIncludingNull(is_null($invitation->getUserId())),
- Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE, ValidationContext::VALIDATION_CONTEXT_FINALIZE]), ['nullable']),
- ],
- 'userGroupsToRemove.*' => [
- 'array',
- new AllowedKeysRule(['userGroupId']),
- ],
- 'userGroupsToRemove.*.userGroupId' => [
- 'distinct',
- 'required',
- 'integer',
- new UserGroupExistsRule(),
- new RemoveUserGroupRule($invitation),
- ],
'userOrcid' => [
Rule::when(in_array($validationContext, [ValidationContext::VALIDATION_CONTEXT_INVITE, ValidationContext::VALIDATION_CONTEXT_FINALIZE]), ['nullable']),
'orcid'
diff --git a/classes/invitation/invitations/userRoleAssignment/resources/UserRoleAssignmentInviteResource.php b/classes/invitation/invitations/userRoleAssignment/resources/UserRoleAssignmentInviteResource.php
index a6eb4f6a073..6d94e27fc8f 100644
--- a/classes/invitation/invitations/userRoleAssignment/resources/UserRoleAssignmentInviteResource.php
+++ b/classes/invitation/invitations/userRoleAssignment/resources/UserRoleAssignmentInviteResource.php
@@ -53,7 +53,6 @@ public function toArray(Request $request)
'emailSubject' => $this->getPayload()->emailSubject,
'emailBody' => $this->getPayload()->emailBody,
'userGroupsToAdd' => $this->transformUserGroups($this->getPayload()->userGroupsToAdd),
- 'userGroupsToRemove' => $this->transformUserGroups($this->getPayload()->userGroupsToRemove),
'username' => $this->getPayload()->username,
'sendEmailAddress' => $this->getPayload()->sendEmailAddress,
'existingUser' => $this->transformUser($this->getExistingUser()),
@@ -62,7 +61,7 @@ public function toArray(Request $request)
}
/**
- * Transform the userGroupsToAdd or userGroupsToRemove to include related UserGroup data.
+ * Transform the userGroupsToAdd to include related UserGroup data.
*
* @param array|null $userGroups
* @return array
@@ -83,7 +82,7 @@ protected function transformUserGroups(?array $userGroups)
}
/**
- * Transform the userGroupsToAdd or userGroupsToRemove to include related UserGroup data.
+ * Transform the userGroupsToAdd to include related UserGroup data.
*
* @param array|null $userGroups
* @return array
diff --git a/classes/invitation/invitations/userRoleAssignment/rules/AddUserGroupRule.php b/classes/invitation/invitations/userRoleAssignment/rules/AddUserGroupRule.php
index bbc4ad4bd68..b6347eb42a9 100644
--- a/classes/invitation/invitations/userRoleAssignment/rules/AddUserGroupRule.php
+++ b/classes/invitation/invitations/userRoleAssignment/rules/AddUserGroupRule.php
@@ -33,6 +33,7 @@ public function passes($attribute, $value)
if ($user = $this->invitation->getExistingUser()) {
$userUserGroups = UserUserGroup::withUserId($user->getId())
->withUserGroupId($value) // The $value is the userGroupId
+ ->withActive()
->get();
return $userUserGroups->isEmpty(); // Fail if the user does have the group assigned
diff --git a/classes/invitation/invitations/userRoleAssignment/rules/NoUserGroupChangesRule.php b/classes/invitation/invitations/userRoleAssignment/rules/NoUserGroupChangesRule.php
index e3208e983ff..fda1120f75b 100644
--- a/classes/invitation/invitations/userRoleAssignment/rules/NoUserGroupChangesRule.php
+++ b/classes/invitation/invitations/userRoleAssignment/rules/NoUserGroupChangesRule.php
@@ -19,21 +19,16 @@
class NoUserGroupChangesRule implements Rule
{
protected ?array $userGroupsToAdd;
- protected ?array $userGroupsToRemove;
protected string $validationContext;
- public function __construct(?array $userGroupsToAdd, ?array $userGroupsToRemove)
+ public function __construct(?array $userGroupsToAdd)
{
$this->userGroupsToAdd = $userGroupsToAdd;
- $this->userGroupsToRemove = $userGroupsToRemove;
}
public function passes($attribute, $value)
{
- return !(
- empty($this->userGroupsToAdd) &&
- empty($this->userGroupsToRemove)
- );
+ return !empty($this->userGroupsToAdd);
}
public function message()
diff --git a/classes/invitation/invitations/userRoleAssignment/rules/PrimaryLocaleRequired.php b/classes/invitation/invitations/userRoleAssignment/rules/PrimaryLocaleRequired.php
new file mode 100644
index 00000000000..20a87f95af2
--- /dev/null
+++ b/classes/invitation/invitations/userRoleAssignment/rules/PrimaryLocaleRequired.php
@@ -0,0 +1,46 @@
+primaryLocale = $primaryLocale;
+ }
+
+ public function passes($attribute, $value)
+ {
+ $providedLocales = array_keys($value);
+ if (!empty($providedLocales) &&
+ array_key_exists($this->primaryLocale, $value) &&
+ empty($value[$this->primaryLocale])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function message()
+ {
+ return __('invitation.userRoleAssignment.validation.error.multilingual.primaryLocaleRequired', [
+ 'primaryLocale' => $this->primaryLocale,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/classes/invitation/invitations/userRoleAssignment/rules/RemoveUserGroupRule.php b/classes/invitation/invitations/userRoleAssignment/rules/RemoveUserGroupRule.php
deleted file mode 100644
index a5e34704ca0..00000000000
--- a/classes/invitation/invitations/userRoleAssignment/rules/RemoveUserGroupRule.php
+++ /dev/null
@@ -1,48 +0,0 @@
-invitation = $invitation;
- }
-
- public function passes($attribute, $value)
- {
- // At this point, we know the user group exists; check if the user has it assigned
- if ($user = $this->invitation->getExistingUser()) {
- $userUserGroups = UserUserGroup::withUserId($user->getId())
- ->withUserGroupId($value) // The $value is the userGroupId
- ->get();
-
- return !$userUserGroups->isEmpty(); // Fail if the user doesn't have the group assigned
- }
-
- return false; // Fail if the user doesn't exist or isn't assigned the group
- }
-
- public function message()
- {
- return __('invitation.userRoleAssignment.validation.error.removeUserRoles.userGroupNotAssignedToUser');
- }
-}
\ No newline at end of file
diff --git a/classes/invitation/sections/Email.php b/classes/invitation/sections/Email.php
new file mode 100644
index 00000000000..614b1c05a8e
--- /dev/null
+++ b/classes/invitation/sections/Email.php
@@ -0,0 +1,109 @@
+ $recipients One or more User objects who are the recipients of this email
+ * @param Mailable $mailable The mailable that will be used to send this email
+ *
+ * @throws Exception
+ */
+ public function __construct(string $id, string $name, string $description, array $recipients, Mailable $mailable, array $locales)
+ {
+ parent::__construct($id, $name, $description);
+ $this->locales = $locales;
+ $this->mailable = $mailable;
+ $this->recipients = $recipients;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getState(): stdClass
+ {
+ $config = parent::getState();
+ $config->canChangeRecipients = false;
+ $config->canSkip = false;
+ $config->emailTemplates = $this->getEmailTemplates();
+ $config->initialTemplateKey = $this->mailable::getEmailTemplateKey();
+ $config->recipientOptions = $this->getRecipientOptions();
+ $config->anonymousRecipients = $this->anonymousRecipients;
+ $config->variables = [];
+ $config->locale = Locale::getLocale();
+ $config->locales = [];
+ return $config;
+ }
+
+ /**
+ * Get all email recipients for email composer
+ * @return array
+ */
+ protected function getRecipientOptions(): array
+ {
+ $recipientOptions = [];
+ foreach ($this->recipients as $user) {
+ $names = [];
+ foreach ($this->locales as $locale) {
+ $names[$locale] = $user->getFullName(true, false, $locale);
+ }
+ $recipientOptions[] = [
+ 'value' => $user->getId(),
+ 'label' => $names,
+ ];
+ }
+ return $recipientOptions;
+ }
+
+ /**
+ * Get all email templates for email composer
+ * @return array
+ */
+ protected function getEmailTemplates(): array
+ {
+ $request = Application::get()->getRequest();
+ $context = $request->getContext();
+
+ $emailTemplates = collect();
+ if ($this->mailable::getEmailTemplateKey()) {
+ $emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $this->mailable::getEmailTemplateKey());
+ if ($emailTemplate) {
+ $emailTemplates->add($emailTemplate);
+ }
+ Repo::emailTemplate()
+ ->getCollector($context->getId())
+ ->alternateTo([$this->mailable::getEmailTemplateKey()])
+ ->getMany()
+ ->each(fn (EmailTemplate $e) => $emailTemplates->add($e));
+ }
+
+ return Repo::emailTemplate()->getSchemaMap()->mapMany($emailTemplates)->toArray();
+ }
+}
diff --git a/classes/invitation/sections/Form.php b/classes/invitation/sections/Form.php
new file mode 100644
index 00000000000..01c58912b6d
--- /dev/null
+++ b/classes/invitation/sections/Form.php
@@ -0,0 +1,49 @@
+form = $form;
+ }
+
+ /**
+ * @inheritDoc
+ * @throws Exception
+ */
+ public function getState(): stdClass
+ {
+ $config = parent::getState();
+ foreach ($this->form->getConfig() as $key => $value) {
+ $config->$key = $value;
+ }
+ unset($config->pages[0]['submitButton']);
+
+ return $config;
+ }
+}
diff --git a/classes/invitation/sections/Section.php b/classes/invitation/sections/Section.php
new file mode 100644
index 00000000000..89fe3e96d42
--- /dev/null
+++ b/classes/invitation/sections/Section.php
@@ -0,0 +1,54 @@
+id = $id;
+ $this->name = $name;
+ $this->description = $description;
+ if (!isset($this->type)) {
+ throw new Exception('Decision workflow step created without specifying a type.');
+ }
+ }
+
+ /**
+ * Compile initial state data to pass to the frontend
+ */
+ public function getState(): stdClass
+ {
+ $config = new stdClass();
+ $config->id = $this->id;
+ $config->type = $this->type;
+ $config->name = $this->name;
+ $config->description = $this->description;
+ $config->errors = new stdClass();
+
+ return $config;
+ }
+}
diff --git a/classes/invitation/sections/Sections.php b/classes/invitation/sections/Sections.php
new file mode 100644
index 00000000000..a433adad326
--- /dev/null
+++ b/classes/invitation/sections/Sections.php
@@ -0,0 +1,80 @@
+id = $id;
+ $this->name = $name;
+ $this->description = $description;
+ $this->sectionComponent = $sectionComponent;
+ $this->type = $type;
+ }
+ /**
+ * Add a step to the invitation
+ */
+ public function addSection($section, $props): void
+ {
+ if(is_null($section)) {
+ $this->sections[] = $section;
+ } else {
+ $this->sections[$section->id] = $section;
+ }
+ $this->props = $props;
+ }
+
+ /**
+ * get section states
+ */
+ public function getState(): array
+ {
+ $state = [];
+ foreach ($this->sections as $section) {
+ if(is_null($section)) {
+ $props = [
+ ...$this->props
+ ];
+ } else {
+ $props = [
+ ...$this->props,
+ $section->type => $section->getState(),
+ ];
+ }
+ $state[] = [
+ 'id' => $this->id,
+ 'name' => $this->name,
+ 'description' => $this->description,
+ 'sectionComponent' => $this->sectionComponent,
+ 'props' => $props,
+ ];
+ }
+ return $state;
+ }
+}
diff --git a/classes/invitation/stepTypes/AcceptInvitationStep.php b/classes/invitation/stepTypes/AcceptInvitationStep.php
new file mode 100644
index 00000000000..ec1783ab7b3
--- /dev/null
+++ b/classes/invitation/stepTypes/AcceptInvitationStep.php
@@ -0,0 +1,212 @@
+getData('orcidAccessToken')) {
+ $steps[] = $this->verifyOrcidStep();
+ $steps[] = $this->acceptInvitationReviewStep($context);
+ }
+ break;
+ default:
+ $steps[] = $this->verifyOrcidStep();
+ $steps[] = $this->userAccountDetailsStep();
+ $steps[] = $this->userDetailsStep($context);
+ $steps[] = $this->acceptInvitationReviewStep($context);
+ }
+ return $steps;
+ }
+
+ /**
+ * user orcid verification step
+ */
+ private function verifyOrcidStep(): \stdClass
+ {
+ $sections = new Sections(
+ 'userVerifyOrcid',
+ __('acceptInvitation.verifyOrcid.stepName'),
+ 'popup',
+ 'AcceptInvitationVerifyOrcid',
+ __('userInvitation.searchUser.stepDescription'),
+ );
+ $sections->addSection(
+ null,
+ [
+ 'validateFields' => ['userOrcid']
+ ]
+ );
+ $step = new Step(
+ 'verifyOrcid',
+ __('acceptInvitation.verifyOrcid.stepName'),
+ __('acceptInvitation.verifyOrcid.stepLabel'),
+ __('userInvitation.verifyOrcid.nextButtonLabel'),
+ 'popup',
+ __('acceptInvitation.verifyOrcid.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * user account details step
+ */
+ private function userAccountDetailsStep(): \stdClass
+ {
+ $sections = new Sections(
+ 'userCreateForm',
+ __('acceptInvitation.accountDetails.stepName'),
+ 'form',
+ 'AcceptInvitationUserAccountDetails',
+ __('userInvitation.accountDetails.stepDescription'),
+ );
+ $sections->addSection(
+ null,
+ [
+ 'validateFields' => [
+ 'username',
+ 'password',
+ 'privacyStatement'
+ ]
+ ]
+ );
+ $step = new Step(
+ 'userCreate',
+ __('acceptInvitation.accountDetails.stepName'),
+ __('acceptInvitation.accountDetails.stepLabel'),
+ __('acceptInvitation.accountDetails.nextButtonLabel'),
+ 'form',
+ __('acceptInvitation.accountDetails.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * user details form step
+ *
+ * @throws \Exception
+ */
+ private function userDetailsStep(Context $context): \stdClass
+ {
+ $sections = new Sections(
+ 'userCreateForm',
+ __('acceptInvitation.accountDetails.stepName'),
+ 'form',
+ 'AcceptInvitationUserDetailsForms',
+ __('userInvitation.accountDetails.stepDescription'),
+ );
+ $sections->addSection(
+ new Form(
+ 'userDetails',
+ __('acceptInvitation.userDetails.form.name'),
+ __('acceptInvitation.userDetails.form.description'),
+ new AcceptUserDetailsForm('accept', $this->getFormLocals($context)),
+ ),
+ [
+ 'validateFields' => [
+ 'affiliation',
+ 'givenName',
+ 'familyName',
+ 'userCountry',
+ ]
+ ]
+ );
+ $step = new Step(
+ 'userDetails',
+ __('acceptInvitation.userDetails.stepName'),
+ __('acceptInvitation.userDetails.stepLabel'),
+ __('acceptInvitation.userDetails.nextButtonLabel'),
+ 'form',
+ __('acceptInvitation.userDetails.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * review details and accept invitation step
+ *
+ * @throws \Exception
+ */
+ private function acceptInvitationReviewStep(Context $context): \stdClass
+ {
+ $sections = new Sections(
+ 'userCreateRoles',
+ '',
+ 'table',
+ 'AcceptInvitationReview',
+ ''
+ );
+ $sections->addSection(
+ new Form(
+ 'userDetails',
+ __('acceptInvitation.userDetails.form.name'),
+ __('acceptInvitation.userDetails.form.description'),
+ new AcceptUserDetailsForm('accept', $this->getFormLocals($context)),
+ ),
+ [
+ 'validateFields' => [
+
+ ]
+ ]
+ );
+ $step = new Step(
+ 'userCreateReview',
+ __('acceptInvitation.detailsReview.stepName'),
+ __('acceptInvitation.detailsReview.stepLabel'),
+ __('acceptInvitation.detailsReview.nextButtonLabel'),
+ 'review',
+ __('acceptInvitation.detailsReview.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * Get all form locals
+ * @param Context $context
+ * @return array
+ */
+ private function getFormLocals(Context $context): array
+ {
+ $localeNames = $context->getSupportedFormLocaleNames();
+ $locales = [];
+ foreach ($localeNames as $key => $name) {
+ $locales[] = [
+ 'key' => $key,
+ 'label' => $name,
+ ];
+ }
+ return $locales;
+ }
+}
diff --git a/classes/invitation/stepTypes/InvitationStepTypes.php b/classes/invitation/stepTypes/InvitationStepTypes.php
new file mode 100644
index 00000000000..31125e49408
--- /dev/null
+++ b/classes/invitation/stepTypes/InvitationStepTypes.php
@@ -0,0 +1,35 @@
+invitationSearchUser();
+ }
+ $steps[] = $this->invitationDetailsForm($context);
+ $steps[] = $this->invitationInvitedEmail($context);
+ return $steps;
+ }
+
+ /**
+ * create search user section
+ */
+ private function invitationSearchUser(): stdClass
+ {
+ $sections = new Sections(
+ 'searchUserForm',
+ __('userInvitation.searchUser.stepName'),
+ 'form',
+ 'UserInvitationSearchFormStep',
+ __('userInvitation.searchUser.stepDescription'),
+ );
+ $sections->addSection(
+ null,
+ [
+ 'validateFields' => []
+ ]
+ );
+ $step = new Step(
+ 'searchUser',
+ __('userInvitation.searchUser.stepName'),
+ __('userInvitation.searchUser.stepLabel'),
+ __('userInvitation.searchUser.nextButtonLabel'),
+ 'emptySection',
+ __('userInvitation.searchUser.stepDescription'),
+ true
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * create user details form section
+ *
+ * @throws Exception
+ */
+ private function invitationDetailsForm(Context $context): stdClass
+ {
+ $localeNames = $context->getSupportedFormLocaleNames();
+ $locales = [];
+ foreach ($localeNames as $key => $name) {
+ $locales[] = [
+ 'key' => $key,
+ 'label' => $name,
+ ];
+ }
+ $sections = new Sections(
+ 'userDetails',
+ __('userInvitation.enterDetails.stepName'),
+ 'form',
+ 'UserInvitationDetailsFormStep',
+ __('userInvitation.enterDetails.stepDescription'),
+ );
+ $sections->addSection(
+ new Form(
+ 'userDetails',
+ __('userInvitation.enterDetails.stepName'),
+ __('userInvitation.enterDetails.stepDescription'),
+ new UserDetailsForm('users', $locales),
+ ),
+ [
+ 'validateFields' => [],
+ 'userGroups' => $this->getAllUserGroups($context)
+ ]
+ );
+ $step = new Step(
+ 'userDetails',
+ __('userInvitation.enterDetails.stepName'),
+ __('userInvitation.enterDetails.stepLabel'),
+ __('userInvitation.enterDetails.nextButtonLabel'),
+ 'form',
+ __('userInvitation.enterDetails.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * create email composer for send invite
+ *
+ * @throws Exception
+ */
+ private function invitationInvitedEmail(Context $context): stdClass
+ {
+ $sections = new Sections(
+ 'userInvitedEmail',
+ __('userInvitation.sendMail.stepLabel'),
+ 'email',
+ 'UserInvitationEmailComposerStep',
+ __('userInvitation.sendMail.stepName'),
+ );
+ $fakeInvitation = $this->getFakeInvitation();
+ $mailable = new UserRoleAssignmentInvitationNotify($context, $fakeInvitation);
+ $sections->addSection(
+ new Email(
+ 'userInvited',
+ __('userInvitation.sendMail.stepName'),
+ __('userInvitation.sendMail.stepDescription'),
+ [],
+ $mailable
+ ->sender(Application::get()->getRequest()->getUser())
+ ->cc('')
+ ->bcc(''),
+ $context->getSupportedFormLocales(),
+ ),
+ [
+ 'validateFields' => []
+ ]
+ );
+ $step = new Step(
+ 'userInvited',
+ __('userInvitation.sendMail.stepName'),
+ __('userInvitation.sendMail.stepLabel'),
+ __('userInvitation.sendMail.nextButtonLabel'),
+ 'email',
+ __('userInvitation.sendMail.stepDescription'),
+ );
+ $step->addSectionToStep($sections->getState());
+ return $step->getState();
+ }
+
+ /**
+ * Get all user groups
+ * @param Context $context
+ * @return array
+ */
+ private function getAllUserGroups(Context $context): array
+ {
+ $allUserGroups = [];
+ $userGroups = Repo::userGroup()->getCollector()
+ ->filterByContextIds([$context->getId()])
+ ->getMany();
+ foreach ($userGroups as $userGroup) {
+ $allUserGroups[] = [
+ 'value' => (int) $userGroup->getId(),
+ 'label' => $userGroup->getLocalizedName(),
+ 'disabled' => false
+ ];
+ }
+ return $allUserGroups;
+ }
+}
diff --git a/classes/invitation/steps/Step.php b/classes/invitation/steps/Step.php
new file mode 100644
index 00000000000..d21ee51980d
--- /dev/null
+++ b/classes/invitation/steps/Step.php
@@ -0,0 +1,73 @@
+id = $id;
+ $this->name = $name;
+ $this->description = $description;
+ $this->stepLabel = $stepLabel;
+ $this->nextButtonLabel = $nextButtonLabel;
+ $this->type = $type;
+ $this->skipInvitationUpdate = $skipInvitationUpdate;
+ }
+
+ /**
+ * Compile initial state data to pass to the frontend
+ */
+ public function getState(): stdClass
+ {
+ $config = new stdClass();
+ $config->id = $this->id;
+ $config->name = $this->name;
+ $config->description = $this->description;
+ $config->nextButtonLabel = $this->nextButtonLabel;
+ $config->skipInvitationUpdate = $this->skipInvitationUpdate;
+ $config->type = $this->type;
+ $config->stepLabel = $this->stepLabel;
+ $config->sections = $this->sections;
+ return $config;
+ }
+
+ /**
+ * Add a step to the workflow
+ */
+ public function addSectionToStep($sections): void
+ {
+ $this->sections = $sections;
+ }
+}
diff --git a/classes/mail/mailables/UserRoleAssignmentInvitationNotify.php b/classes/mail/mailables/UserRoleAssignmentInvitationNotify.php
index bf45a7c6bb6..d11ee00fd8d 100644
--- a/classes/mail/mailables/UserRoleAssignmentInvitationNotify.php
+++ b/classes/mail/mailables/UserRoleAssignmentInvitationNotify.php
@@ -55,7 +55,6 @@ class UserRoleAssignmentInvitationNotify extends Mailable
protected static string $inviterName = 'inviterName';
protected static string $inviterRole = 'inviterRole';
protected static string $rolesAdded = 'rolesAdded';
- protected static string $rolesRemoved = 'rolesRemoved';
protected static string $existingRoles = 'existingRoles';
protected static string $acceptUrl = 'acceptUrl';
protected static string $declineUrl = 'declineUrl';
@@ -80,7 +79,6 @@ public static function getDataDescriptions(): array
$variables[static::$inviterName] = __('emailTemplate.variable.invitation.inviterName');
$variables[static::$inviterRole] = __('emailTemplate.variable.invitation.inviterRole');
$variables[static::$rolesAdded] = __('emailTemplate.variable.invitation.rolesAdded');
- $variables[static::$rolesRemoved] = __('emailTemplate.variable.invitation.rolesRemoved');
$variables[static::$existingRoles] = __('emailTemplate.variable.invitation.existingRoles');
$variables[static::$acceptUrl] = __('emailTemplate.variable.invitation.acceptUrl');
$variables[static::$declineUrl] = __('emailTemplate.variable.invitation.declineUrl');
@@ -183,24 +181,9 @@ public function setData(?string $locale = null): void
$existingUserGroupsTitle = __('emails.userRoleAssignmentInvitationNotify.alreadyAssignedRoles');
- $userGroupsRemovedTitle = __('emails.userRoleAssignmentInvitationNotify.removedRoles');
$existingUserGroups = '';
- $userGroupsRemoved = '';
if (isset($user)) {
- // Roles Removed
- foreach ($this->invitation->getPayload()->userGroupsToRemove as $userUserGroup) {
- $userGroupHelper = UserGroupHelper::fromArray($userUserGroup);
-
- $userGroup = Repo::userGroup()->get($userGroupHelper->userGroupId);
- $userUserGroups = UserUserGroup::withUserId($user->getId())
- ->withUserGroupId($userGroup->getId())
- ->withActive()
- ->get();
-
- $userGroupsRemoved = $this->getAllUserUserGroupSection($userUserGroups->toArray(), $userGroup, $context, $locale, $userGroupsRemovedTitle);
- }
-
// Existing Roles
$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$this->invitation->getContextId()])
@@ -231,7 +214,6 @@ public function setData(?string $locale = null): void
static::$acceptUrl => $this->invitation->getActionURL(InvitationAction::ACCEPT),
static::$declineUrl => $this->invitation->getActionURL(InvitationAction::DECLINE),
static::$rolesAdded => $userGroupsAdded,
- static::$rolesRemoved => $userGroupsRemoved,
static::$existingRoles => $existingUserGroups,
static::EMAIL_TEMPLATE_STYLE_PROPERTY => $emailTemplateStyle,
]
diff --git a/classes/submission/maps/Schema.php b/classes/submission/maps/Schema.php
index 0924f1915f1..54f78ccecab 100644
--- a/classes/submission/maps/Schema.php
+++ b/classes/submission/maps/Schema.php
@@ -20,9 +20,9 @@
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\db\DAORegistry;
+use PKP\decision\DecisionType;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
-use PKP\query\Query;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
@@ -31,6 +31,7 @@
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
+use PKP\user\User;
use PKP\userGroup\UserGroup;
use PKP\workflow\WorkflowStageDAO;
@@ -527,7 +528,7 @@ public function getPropertyStages(Submission $submission): array
$currentUserAssignedRoles = [];
$stageAssignmentsOverview = [];
if ($currentUser) {
- // FIXME - $stageAssignments are just temporarly added until https://github.com/pkp/pkp-lib/issues/10480 is ready
+ // FIXME - $stageAssignments are just temporarily added until https://github.com/pkp/pkp-lib/issues/10480 is ready
$currentRoles = array_map(
function (Role $role) {
return $role->getId();
@@ -556,10 +557,10 @@ function (Role $role) {
foreach ($stageAssignments as $stageAssignment) {
$userGroup = Repo::userGroup()->get($stageAssignment->userGroupId);
$stageAssignmentsOverview[] = [
- "roleId" => $userGroup->getRoleId(),
- "recommendOnly" => $stageAssignment->recommendOnly,
- "canChangeMetadata" => $stageAssignment->canChangeMetadata,
- "userId" => $stageAssignment->userId
+ 'roleId' => $userGroup->getRoleId(),
+ 'recommendOnly' => $stageAssignment->recommendOnly,
+ 'canChangeMetadata' => $stageAssignment->canChangeMetadata,
+ 'userId' => $stageAssignment->userId
];
}
}
@@ -568,7 +569,7 @@ function (Role $role) {
$stage['stageAssignments'] = $stageAssignmentsOverview;
if(!$stage['currentUserAssignedRoles']) {
if(in_array(Role::ROLE_ID_MANAGER, $currentRoles)) {
- $stage['currentUserAssignedRoles'][] = Role::ROLE_ID_MANAGER;
+ $stage['currentUserAssignedRoles'][] = Role::ROLE_ID_MANAGER;
}
}
// Stage-specific statuses
@@ -649,6 +650,10 @@ function (Role $role) {
break;
}
+
+ $availableEditorialDecisions = $this->getAvailableEditorialDecisions($stageId, $submission);
+ $stage['availableEditorialDecisions'] = array_map(fn (DecisionType $decisionType) => ['id' => $decisionType->getDecision(), 'label' => $decisionType->getLabel()], $availableEditorialDecisions);
+
$stages[] = $stage;
}
@@ -722,4 +727,69 @@ protected function appSpecificProps(): array
{
return [];
}
+
+ /**
+ * Implement by a child class to get available editorial decisions data for a stage of a submission.
+ */
+ protected function getAvailableEditorialDecisions(int $stageId, Submission $submission): array
+ {
+ return [];
+ }
+
+ /**
+ * Check if a user can make Decisions or Recommendations on a submission's stage
+ */
+ protected function checkDecisionPermissions(int $stageId, Submission $submission, User $user, int $contextId): array
+ {
+ /** @var StageAssignment[] $editorsStageAssignments*/
+ $editorsStageAssignments = StageAssignment::withSubmissionIds([$submission->getId()])
+ ->withStageIds([$stageId])
+ ->withRoleIds([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR])
+ ->withUserId($user->getId())
+ ->get();
+
+ $makeRecommendation = $makeDecision = false;
+ // if the user is assigned several times in an editorial role, check his/her assignments permissions i.e.
+ // if the user is assigned with both possibilities: to only recommend as well as make decision
+ foreach ($editorsStageAssignments as $editorsStageAssignment) {
+ if (!$editorsStageAssignment->recommendOnly) {
+ $makeDecision = true;
+ } else {
+ $makeRecommendation = true;
+ }
+ }
+
+ // If user is not assigned to the submission,
+ // see if the user is manager, and
+ // if the group is recommendOnly
+ if (!$makeRecommendation && !$makeDecision) {
+ $userGroups = Repo::userGroup()->userUserGroups($user->getId(), $contextId);
+ foreach ($userGroups as $userGroup) {
+ if (in_array($userGroup->getRoleId(), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN])) {
+ if (!$userGroup->getRecommendOnly()) {
+ $makeDecision = true;
+ } else {
+ $makeRecommendation = true;
+ }
+ }
+ }
+ }
+
+ // if the user can make recommendations, check whether there are any decisions that can be made given
+ // the stage that we are operating into.
+ $isOnlyRecommending = $makeRecommendation && !$makeDecision;
+
+ if ($isOnlyRecommending) {
+ if (!empty(Repo::decision()->getDecisionTypesMadeByRecommendingUsers($stageId))) {
+ // If there are any, then the user can be considered a decision user.
+ $makeDecision = true;
+ }
+ }
+
+ return [
+ 'canMakeDecision' => $makeDecision,
+ 'canMakeRecommendation' => $makeRecommendation,
+ 'isOnlyRecommending' => $isOnlyRecommending
+ ];
+ }
}
diff --git a/classes/template/PKPTemplateManager.php b/classes/template/PKPTemplateManager.php
index d458dfebc8a..a1161ab081e 100644
--- a/classes/template/PKPTemplateManager.php
+++ b/classes/template/PKPTemplateManager.php
@@ -578,6 +578,7 @@ public function addJavaScript(string $name, string $script, array $args = []): v
'priority' => self::STYLE_SEQUENCE_NORMAL,
'contexts' => ['frontend'],
'inline' => false,
+ 'type' => 'text/javascript',
],
$args
);
@@ -587,6 +588,7 @@ public function addJavaScript(string $name, string $script, array $args = []): v
$this->_javaScripts[$context][$args['priority']][$name] = [
'script' => $script,
'inline' => $args['inline'],
+ 'type' => $args['type'],
];
}
}
@@ -2196,12 +2198,12 @@ public function smartyLoadScript($params, $smarty)
foreach ($scripts as $priorityList) {
foreach ($priorityList as $name => $data) {
if ($data['inline']) {
- $output .= '';
+ $output .= '';
} else {
if ($appVersion && strpos($data['script'], '?') === false) {
$data['script'] .= '?v=' . $appVersion;
}
- $output .= '';
+ $output .= '';
}
}
}
diff --git a/classes/user/Collector.php b/classes/user/Collector.php
index 804deb05179..f00b529e82d 100644
--- a/classes/user/Collector.php
+++ b/classes/user/Collector.php
@@ -47,7 +47,7 @@ class Collector implements CollectorInterface
public const STATUS_ACTIVE = 'active';
public const STATUS_DISABLED = 'disabled';
- public const STATUS_ALL = null;
+ public const STATUS_ALL = 'all';
public DAO $dao;
@@ -488,6 +488,10 @@ protected function buildUserGroupFilter(Builder $query): self
}
$currentDateTime = Core::getCurrentDate();
+ if ($this->status === self::STATUS_ALL) {
+ $this->userUserGroupStatus = UserUserGroupStatus::STATUS_ALL;
+ }
+
$subQuery = DB::table('user_user_groups as uug')
->join('user_groups AS ug', 'uug.user_group_id', '=', 'ug.user_group_id')
->whereColumn('uug.user_id', '=', 'u.user_id')
diff --git a/classes/xml/XMLParserDOMHandler.php b/classes/xml/XMLParserDOMHandler.php
index b32b05d5759..e3580e34fdd 100644
--- a/classes/xml/XMLParserDOMHandler.php
+++ b/classes/xml/XMLParserDOMHandler.php
@@ -24,13 +24,13 @@
class XMLParserDOMHandler extends XMLParserHandler
{
- /** @var Root node */
+ /** @var ?XMLNode Root node */
public ?XMLNode $rootNode;
- /** @var The node currently being parsed */
+ /** @var ?XMLNode The node currently being parsed */
public ?XMLNode $currentNode = null;
- /** @var string reference to the current data */
+ /** @var ?string reference to the current data */
public ?string $currentData = null;
/** @var XMLNode[] */
@@ -46,7 +46,7 @@ public function __construct()
/**
* Callback function to act as the start element handler.
*/
- public function startElement(XMLParser|PKPXMLParser $parser, string $tag, array $attributes) : void
+ public function startElement(XMLParser|PKPXMLParser $parser, string $tag, array $attributes): void
{
$this->currentData = null;
$node = new XMLNode($tag);
@@ -65,7 +65,7 @@ public function startElement(XMLParser|PKPXMLParser $parser, string $tag, array
/**
* Callback function to act as the end element handler.
*/
- public function endElement(XMLParser|PKPXMLParser $parser, string $tag)
+ public function endElement(XMLParser|PKPXMLParser $parser, string $tag): void
{
$this->currentNode->setValue($this->currentData);
$this->currentNode = & $this->currentNode->getParent();
@@ -75,7 +75,7 @@ public function endElement(XMLParser|PKPXMLParser $parser, string $tag)
/**
* Callback function to act as the character data handler.
*/
- public function characterData(XMLParser|PKPXMLParser $parser, string $data)
+ public function characterData(XMLParser|PKPXMLParser $parser, string $data): void
{
$this->currentData .= $data;
}
@@ -83,7 +83,7 @@ public function characterData(XMLParser|PKPXMLParser $parser, string $data)
/**
* Returns a reference to the root node of the tree representing the document.
*/
- public function getResult() : mixed
+ public function getResult(): mixed
{
return $this->rootNode;
}
diff --git a/classes/xml/XMLParserHandler.php b/classes/xml/XMLParserHandler.php
index ddb5741bd5a..e63af2f4941 100644
--- a/classes/xml/XMLParserHandler.php
+++ b/classes/xml/XMLParserHandler.php
@@ -42,7 +42,7 @@ public function characterData(PKPXMLParser $parser, string $data)
* Returns a resulting data structure representing the parsed content.
* The format of this object is specific to the handler.
*/
- abstract public function getResult() : mixed;
+ abstract public function getResult(): mixed;
}
if (!PKP_STRICT_MODE) {
diff --git a/controllers/grid/settings/library/form/NewLibraryFileForm.php b/controllers/grid/settings/library/form/NewLibraryFileForm.php
index 51dd699e75e..9caee989540 100644
--- a/controllers/grid/settings/library/form/NewLibraryFileForm.php
+++ b/controllers/grid/settings/library/form/NewLibraryFileForm.php
@@ -43,10 +43,10 @@ public function __construct(int $contextId)
*
* @see Form::readInputData()
*/
- public function readInputData()
+ public function readInputData(): void
{
$this->readUserVars(['temporaryFileId']);
- return parent::readInputData();
+ parent::readInputData();
}
/**
diff --git a/controllers/grid/users/reviewer/form/ThankReviewerForm.php b/controllers/grid/users/reviewer/form/ThankReviewerForm.php
index fd23eab0b20..ee937275080 100644
--- a/controllers/grid/users/reviewer/form/ThankReviewerForm.php
+++ b/controllers/grid/users/reviewer/form/ThankReviewerForm.php
@@ -125,7 +125,7 @@ public function execute(...$functionArgs)
Hook::call('ThankReviewerForm::thankReviewer', [$submission, $reviewAssignment, $mailable]);
- (new SendReviewToOrcid($submission, $context, $reviewAssignment))->execute();
+ (new SendReviewToOrcid($reviewAssignment->getId()))->execute();
if (!$this->getData('skipEmail')) {
$mailable->setData(Locale::getLocale());
diff --git a/js/load.js b/js/load.js
index d82c54741b6..1ee1f943881 100644
--- a/js/load.js
+++ b/js/load.js
@@ -106,6 +106,9 @@ import FieldSlider from '@/components/Form/fields/FieldSlider.vue';
// Panel components from UI Library
import ListPanel from '@/components/ListPanel/ListPanel.vue';
+// Manager components
+import UserInvitationManager from '@/managers/UserInvitationManager/UserInvitationManager.vue';
+
// Helper for initializing and tracking Vue controllers
import VueRegistry from './classes/VueRegistry.js';
@@ -216,6 +219,9 @@ VueRegistry.registerComponent('field-pub-id', FieldPubId);
// Register ListPanel
VueRegistry.registerComponent('PkpListPanel', ListPanel);
+// Register Invitation Manager
+VueRegistry.registerComponent('UserInvitationManager', UserInvitationManager);
+
const pinia = createPinia();
function pkpCreateVueApp(createAppArgs) {
diff --git a/locale/en/common.po b/locale/en/common.po
index 80226c66569..e2f967f80eb 100644
--- a/locale/en/common.po
+++ b/locale/en/common.po
@@ -172,6 +172,9 @@ msgstr "Editorial History Page"
msgid "common.editorialHistory.page.description"
msgstr "This section lists past contributors."
+msgid "common.editorialHistory.page.orcidLink"
+msgstr "View {$name} ORCID profile"
+
msgid "common.name"
msgstr "Name"
@@ -2339,4 +2342,4 @@ msgid "common.sandbox"
msgstr "Application running in sandbox mode."
msgid "common.fromUntil"
-msgstr "{$from} - {$until}"
+msgstr "{$from} – {$until}"
diff --git a/locale/en/emails.po b/locale/en/emails.po
index e780ab5cbf0..69bfa81bae2 100644
--- a/locale/en/emails.po
+++ b/locale/en/emails.po
@@ -575,7 +575,6 @@ msgstr ""
" At {$contextName}, we value your privacy. As such, we have taken steps to ensure that we are fully GDPR compliant. These steps include you being accountable to enter your own data and choosing who can see what information. or additional information on how we handled your data, please refer to our Privacy Policy.
"
" {$existingRoles}
"
" {$rolesAdded}
"
-" {$rolesRemoved}
"
" On accepting the invite, you will be redirected to {$contextName}
"
" Feel free to contact me with any questions about the process.
"
" Accept Invitation
"
@@ -621,10 +620,6 @@ msgstr "Newly assigned roles
"
msgid "emails.userRoleAssignmentInvitationNotify.alreadyAssignedRoles"
msgstr "Already assigned roles
"
-#, fuzzy
-msgid "emails.userRoleAssignmentInvitationNotify.removedRoles"
-msgstr "Removed roles
"
-
#, fuzzy
msgid "emailTemplate.variable.invitation.recipientName"
msgstr "This is the name of the invitation recipient"
@@ -641,10 +636,6 @@ msgstr "This is the role of the sender of the invitation"
msgid "emailTemplate.variable.invitation.rolesAdded"
msgstr "This section describes the roles that will be added to the user upon the invitation acceptance"
-#, fuzzy
-msgid "emailTemplate.variable.invitation.rolesRemoved"
-msgstr "This section describes the roles that will be removed from the user upon the invitation acceptance"
-
#, fuzzy
msgid "emailTemplate.variable.invitation.existingRoles"
msgstr "This section describes the roles that the user already has."
diff --git a/locale/en/invitation.po b/locale/en/invitation.po
index 853e431cbf4..f822f8d7dc2 100644
--- a/locale/en/invitation.po
+++ b/locale/en/invitation.po
@@ -25,9 +25,6 @@ msgstr "At least one user group to remove must be defined"
msgid "invitation.userRoleAssignment.validation.error.removeUserRoles.cantRemoveFromNonExistingUser"
msgstr "Remove user roles can't be defined for not existing users."
-msgid "invitation.userRoleAssignment.validation.error.removeUserRoles.userGroupNotAssignedToUser"
-msgstr "The user group is not assigned to the invited user"
-
msgid "invitation.userRoleAssignment.validation.error.noUserGroupChanges"
msgstr "The invitation can not be dispatched because you have not defined any user group changes"
@@ -101,4 +98,262 @@ msgid "invitation.userRoleAssignment.userGroup.startDate.mustBeAfterToday"
msgstr "This attribute must have a value equal or after today"
msgid "invitation.validation.error.propertyProhibited"
-msgstr "The :attribute field is prohibited"
\ No newline at end of file
+msgstr "The :attribute field is prohibited"
+
+msgid "invitation.userRoleAssignment.validation.error.multilingual.primaryLocaleRequired"
+msgstr "A value for locale '{$primaryLocale}' is required"
+
+msgid "invitation.step"
+msgstr "STEP"
+
+msgid "invitation.header"
+msgstr "Invitations"
+
+msgid "invitation.inviteToRole.btn"
+msgstr "Invite to a role"
+
+msgid "invitation.wizard.pageTitle"
+msgstr "Invite user to take a role"
+
+msgid "userInvitation.enterDetailsLabel"
+msgstr "Enter details"
+
+msgid "userInvitation.reviewAndInviteLabel"
+msgstr "Review & invite for roles"
+
+msgid "userInvitation.searchUser.stepName"
+msgstr "Search User"
+
+msgid "userInvitation.searchUser.stepLabel"
+msgstr "{$step} - Search User"
+
+msgid "userInvitation.searchUser.nextButtonLabel"
+msgstr "Search User"
+
+msgid "userInvitation.searchUser.stepDescription"
+msgstr "Search for the user using their email address, username or ORCID ID. Enter at least one details to get started. If the user does not exist, you can invite them to take up roles and be a part of your journal. If the user already exist in the system, you can view user information and invite to take a additional roles."
+
+msgid "userInvitation.emailField.description"
+msgstr "e.g. aeinstein@example.com"
+
+msgid "userInvitation.usernameField.description"
+msgstr "e.g. mickeymouse"
+
+msgid "userInvitation.orcidField.description"
+msgstr "e.g. 0000-0000-0000-0000"
+
+msgid "userInvitation.enterDetails.stepName"
+msgstr "Enter details"
+
+msgid "userInvitation.enterDetails.stepLabel"
+msgstr "{$step} - Enter details and invite for roles"
+
+msgid "userInvitation.enterDetails.nextButtonLabel"
+msgstr "Save And Continue"
+
+msgid "invitation.role.selectRole"
+msgstr "Select a new role"
+
+msgid "invitation.role.dateStart"
+msgstr "Start Date"
+
+msgid "invitation.role.dateEnd"
+msgstr "End Date"
+
+msgid "invitation.role.addRole.button"
+msgstr "Add Another Role"
+
+msgid "userInvitation.roleTable.role"
+msgstr "Role"
+
+msgid "userInvitation.roleTable.startDate"
+msgstr "Start Date"
+
+msgid "userInvitation.roleTable.endDate"
+msgstr "End Date"
+
+msgid "userInvitation.sendMail.stepName"
+msgstr "Review & invite for roles"
+
+msgid "userInvitation.sendMail.nextButtonLabel"
+msgstr "Invite user to the role"
+
+msgid "userInvitation.sendMail.stepLabel"
+msgstr "{$step} - Modify email shared with the user"
+
+msgid "userInvitation.modal.title"
+msgstr "Invitation Sent"
+
+msgid "userInvitation.modal.button"
+msgstr "View All Users"
+
+msgid "invitation.role.removeRole.button"
+msgstr "Remove Role"
+
+msgid "invitation.email.description"
+msgstr "e.g. aeinstein@example.com"
+
+msgid "invitation.orcid.description"
+msgstr "On accepting the invite, the user will be redirected to ORCID to verify their account, if they wish to."
+
+msgid "invitation.givenName.description"
+msgstr "If you know the given name of the user, you can enter the information. However, this information can be changed by the user."
+
+msgid "invitation.familyName.description"
+msgstr "If you know the family name of the user, you can enter the information. However, this information can be changed by the user."
+
+msgid "acceptInvitation.verifyOrcid.stepName"
+msgstr "Verify ORCID iD"
+
+msgid "acceptInvitation.verifyOrcid.stepLabel"
+msgstr "{$step} - Verify ORCID iD"
+
+msgid "acceptInvitation.verifyOrcid.nextButtonLabel"
+msgstr "Save and continue"
+
+msgid "acceptInvitation.accountDetails.nextButtonLabel"
+msgstr "Save and continue"
+
+msgid "acceptInvitation.userDetails.stepName"
+msgstr "Enter details"
+
+msgid "acceptInvitation.userDetails.stepLabel"
+msgstr "{$step} - Enter details"
+
+msgid "acceptInvitation.userDetails.stepDescription"
+msgstr "Enter your details like email ID, affiliation etc. As per the GDPR compliance, this information can only modified by you. You can also choose if you want this information to be visible on your profile to the editor."
+
+msgid "acceptInvitation.userDetails.nextButtonLabel"
+msgstr "Save and continue"
+
+msgid "acceptInvitation.userDetails.form.name"
+msgstr "Accept invitation user details form"
+
+msgid "acceptInvitation.userDetails.form.description"
+msgstr "Please provide the following details to help us to manage your account"
+
+msgid "acceptInvitation.detailsReview.stepName"
+msgstr "Review & create account"
+
+msgid "acceptInvitation.detailsReview.stepLabel"
+msgstr "{$step} - Review & create account"
+
+msgid "acceptInvitation.skipVerifyOrcid"
+msgstr "Skip ORCID verification"
+
+msgid "acceptInvitation.verifyOrcid"
+msgstr "Verify ORCID iD"
+
+msgid "acceptInvitation.usernameField.description"
+msgstr "It could be a combination of uppercase letters, lowercase letters or numbers"
+
+msgid "acceptInvitation.passwordField.description"
+msgstr "It should be at least 6 characters long and could be a combination of uppercase letters, lowercase letters, numbers and symbols"
+
+msgid "acceptInvitation.privacyStatement.label"
+msgstr "Yes, I agree to have my data collected and stored according to the {$url}"
+
+msgid "acceptInvitation.privacyStatement.btn"
+msgstr "Privacy Statement"
+
+msgid "acceptInvitation.userDetailsForm.givenName.description"
+msgstr "Also known as a forename or the first name, it is tha part of a personal name that identifies a person."
+
+msgid "acceptInvitation.userDetailsForm.familyName.description"
+msgstr "A surname, family name, or last name is the mostly hereditary portion of one's personal name that indicates one's family."
+
+msgid "acceptInvitation.userDetailsForm.affiliation.description"
+msgstr "This is the institute you are affiliated with"
+
+msgid "acceptInvitation.userDetailsForm.countryOfAffiliation.description"
+msgstr "This is a country in which the institute you are affiliated with is situated"
+
+msgid "acceptInvitation.userDetailsForm.countryOfAffiliation.label"
+msgstr "Country of affiliation"
+
+msgid "acceptInvitation.review.accountDetails"
+msgstr "Account Details"
+
+msgid "acceptInvitation.review.userDetails"
+msgstr "User Details"
+
+msgid "acceptInvitation.modal.button"
+msgstr "View All Submissions"
+
+msgid "invitation.tableHeader.name"
+msgstr "Name"
+
+msgid "invitation.searchForm.emptyError"
+msgstr "Provide at least one search criteria."
+
+msgid "invitation.wizard.viewPageTitleDescription"
+msgstr "You are viewing {$name}'s user details"
+
+msgid "invitation.reviewerAccess.validation.error.reviewAssignmentId.notExisting"
+msgstr "The id {reviewAssignmentId} does not correspond to a valid review assignment"
+
+msgid "invitation.api.error.invitationCantBeCanceled"
+msgstr "This invitation can't be cancelled"
+
+msgid "invitation.api.error.initialization.noUserIdAndEmailTogether"
+msgstr "You cannot provide both email and userId together."
+
+msgid "invitation.userRoleAssignment.error.update.prohibitedForExistingUser"
+msgstr "The attribute is not allowed to be access for existing users"
+
+msgid "invitation.userRoleAssignment.error.update.prohibitedForNonExistingUser"
+msgstr "The attribute is not allowed to be access for non existing users"
+
+msgid "invitation.userRoleAssignment.userGroup.startDate.mustBeAfterToday"
+msgstr "This attribute must have a value equal or after today"
+
+msgid "invitation.validation.error.propertyProhibited"
+msgstr "The :attribute field is prohibited"
+
+msgid "invitation.cancelInvite.actionName"
+msgstr "Cancel Invite"
+
+msgid "invitation.cancelInvite.title"
+msgstr "Cancel Invitation"
+
+msgid "invitation.cancelInvite.message"
+msgstr "Canceling the invitation sent to {$givenName} {$familyName} will deactivate acceptance link sent via email. Here are the invitation details: "
+
+msgid "invitation.masthead.show"
+msgstr "Appear on the masthead"
+
+msgid "invitation.masthead.hidden"
+msgstr "Does not appear on the masthead"
+
+msgid "invitation.role.modifyRole.button"
+msgstr "Modify Role"
+
+msgid "invitation.management.options"
+msgstr "Invitation management options"
+
+msgid "userInvitation.cancel.message"
+msgstr "Are you sure want to cancel this invitation?"
+
+msgid "userInvitation.cancel.keepWorking"
+msgstr "Keep Working"
+
+msgid "userInvitation.status.invited"
+msgstr "Invited {$date}"
+
+msgid "userInvitation.edit.title"
+msgstr "Edit Invitation"
+
+msgid "userInvitation.edit.message"
+msgstr "If you edit the existing invitation or add a new role, the current invitation will be canceled and, a new one will be sent. Are you sure you want to proceed?"
+
+msgid "validation.after_or_equal"
+msgstr "Start date should be greater than or equal to today"
+
+msgid "invitation.removeRoles"
+msgstr "User Removed From Role"
+
+msgid "acceptInvitation.privacyStatement.validation"
+msgstr "Please confirm that you have read and agree privacy statement"
+
+msgid "acceptInvitation.cancel.message"
+msgstr "Are you sure want to cancel accepting invitation?"
diff --git a/locale/en/user.po b/locale/en/user.po
index 29721f1288b..1d9a96a9848 100644
--- a/locale/en/user.po
+++ b/locale/en/user.po
@@ -788,3 +788,6 @@ msgstr "Are you sure you want to remove this ORCID?"
msgid "orcid.field.unverified.shouldRequest"
msgstr "This ORCID has not been verified. Please remove this unverified ORCID and request verification from the user/author directly."
+
+msgid "user.removeRole.message"
+msgstr "Are you sure want remove this role permanently?"
diff --git a/pages/dashboard/PKPDashboardHandlerNext.php b/pages/dashboard/PKPDashboardHandlerNext.php
index 9d6b3552047..94d69703315 100644
--- a/pages/dashboard/PKPDashboardHandlerNext.php
+++ b/pages/dashboard/PKPDashboardHandlerNext.php
@@ -158,12 +158,14 @@ public function index($args, $request)
'dashboardPage' => $this->dashboardPage,
'countPerPage' => $this->perPage,
'filtersForm' => $filtersForm->getConfig(),
- 'contributorForm' => $contributorForm->getConfig(),
'views' => $this->getViews(),
'columns' => $this->getColumns(),
'publicationSettings' => [
'supportsCitations' => !!$context->getData('citations'),
'identifiersEnabled' => $identifiersEnabled,
+ ],
+ 'componentForms' => [
+ 'contributorForm' => $contributorForm->getConfig(),
]
]
]);
diff --git a/pages/invitation/InvitationHandler.php b/pages/invitation/InvitationHandler.php
index 9874438dee5..28345859a73 100644
--- a/pages/invitation/InvitationHandler.php
+++ b/pages/invitation/InvitationHandler.php
@@ -20,11 +20,16 @@
use APP\core\Request;
use APP\facades\Repo;
use APP\handler\Handler;
+use APP\template\TemplateManager;
+use PKP\core\PKPApplication;
+use PKP\facades\Locale;
use PKP\invitation\core\enums\InvitationAction;
use PKP\invitation\core\Invitation;
+use PKP\invitation\stepTypes\SendInvitationStep;
class InvitationHandler extends Handler
{
+ public $_isBackendPage = true;
public const REPLY_PAGE = 'invitation';
public const REPLY_OP_ACCEPT = 'accept';
public const REPLY_OP_DECLINE = 'decline';
@@ -34,6 +39,7 @@ class InvitationHandler extends Handler
*/
public function accept(array $args, Request $request): void
{
+ $this->setupTemplate($request);
$invitation = $this->getInvitationByKey($request);
$invitationHandler = $invitation->getInvitationActionRedirectController();
$invitationHandler->preRedirectActions(InvitationAction::ACCEPT);
@@ -62,7 +68,23 @@ private function getInvitationByKey(Request $request): Invitation
if (is_null($invitation)) {
$request->getDispatcher()->handle404();
}
+ return $invitation;
+ }
+
+ /**
+ * Get invitation by invitation id
+ * @param Request $request
+ * @param int $id
+ * @return Invitation
+ */
+ private function getInvitationById(Request $request, int $id): Invitation
+ {
+ $invitation = Repo::invitation()
+ ->getById($id);
+ if (is_null($invitation)) {
+ $request->getDispatcher()->handle404('The link is deactivated as the invitation was cancelled');
+ }
return $invitation;
}
@@ -92,4 +114,142 @@ public static function getActionUrl(InvitationAction $action, Invitation $invita
]
);
}
+
+ /**
+ * Create an invitation to accept new role
+ * @param array $args
+ * @param Request $request
+ * @return void
+ * @throws \Exception
+ */
+ public function invite(array $args, Request $request): void
+ {
+ $invitationMode = 'create';
+ $invitationPayload = [
+ 'userId' => null,
+ 'inviteeEmail' => '',
+ 'orcid' => '',
+ 'givenName' => '',
+ 'familyName' => '',
+ 'orcidValidation' => false,
+ 'userGroupsToAdd' => [
+ [
+ 'userGroupId' => null,
+ 'dateStart' => null,
+ 'dateEnd' => null,
+ 'masthead' => null,
+ ]
+ ],
+ 'currentUserGroups' => [],
+ 'userGroupsToRemove' => [],
+ 'emailComposer' => [
+ 'body' => '',
+ 'subject' => '',
+ ]
+ ];
+ $invitation = null;
+ $user = null;
+ if(!empty($args)) {
+ $invitation = $this->getInvitationById($request, $args[0]);
+ $payload = $invitation->getPayload()->toArray();
+ $invitationModel = $invitation->invitationModel->toArray();
+
+ $invitationMode = 'edit';
+ if($invitationModel['userId']){
+ $user = Repo::user()->get($invitationModel['userId']);
+ }
+ $invitationPayload['userId'] = $invitationModel['userId'];
+ $invitationPayload['inviteeEmail'] = $invitationModel['email'] ?: $user->getEmail();
+ $invitationPayload['orcid'] = $payload['orcid'];
+ $invitationPayload['givenName'] = $user ? $user->getGivenName(null) : $payload['givenName'];
+ $invitationPayload['familyName'] = $user ? $user->getFamilyName(null) : $payload['familyName'];
+ $invitationPayload['affiliation'] = $user ? $user->getAffiliation(null) : $payload['affiliation'];
+ $invitationPayload['country'] = $user ? $user->getCountry() : $payload['userCountry'];
+ $invitationPayload['userGroupsToAdd'] = $payload['userGroupsToAdd'];
+ $invitationPayload['currentUserGroups'] = !$invitationModel['userId'] ? [] : $this->getUserUserGroups($invitationModel['userId']);
+ $invitationPayload['userGroupsToRemove'] = !$payload['userGroupsToRemove'] ? null : $payload['userGroupsToRemove'];
+ $invitationPayload['emailComposer'] = [
+ 'emailBody'=>$payload['emailBody'],
+ 'emailSubject'=>$payload['emailSubject'],
+ ];
+ }
+ $templateMgr = TemplateManager::getManager($request);
+ $breadcrumbs = $templateMgr->getTemplateVars('breadcrumbs');
+ $this->setupTemplate($request);
+ $context = $request->getContext();
+ $breadcrumbs[] = [
+ 'id' => 'contexts',
+ 'name' => __('navigation.access'),
+ 'url' => $request
+ ->getDispatcher()
+ ->url(
+ $request,
+ PKPApplication::ROUTE_PAGE,
+ $request->getContext()->getPath(),
+ 'management',
+ 'settings',
+ )
+ ];
+ $breadcrumbs[] = [
+ 'id' => 'invitationWizard',
+ 'name' => __('invitation.wizard.pageTitle'),
+ ];
+ $steps = new SendInvitationStep();
+ $templateMgr->setState([
+ 'steps' => $steps->getSteps($invitation,$context,$user),
+ 'emailTemplatesApiUrl' => $request
+ ->getDispatcher()
+ ->url(
+ $request,
+ Application::ROUTE_API,
+ $context->getData('urlPath'),
+ 'emailTemplates'
+ ),
+ 'primaryLocale' => $context->getData('primaryLocale'),
+ 'invitationType' => 'userRoleAssignment',
+ 'invitationPayload' => $invitationPayload,
+ 'invitationMode' => $invitationMode,
+ 'pageTitle' => $invitation ?
+ (
+ $invitationPayload['givenName'][Locale::getLocale()] . ' '
+ . $invitationPayload['familyName'][Locale::getLocale()])
+ : __('invitation.wizard.pageTitle'),
+ 'pageTitleDescription' => $invitation ?
+ __(
+ 'invitation.wizard.viewPageTitleDescription',
+ ['name' => $invitationPayload['givenName'][Locale::getLocale()]]
+ )
+ : __('invitation.wizard.pageTitleDescription'),
+ ]);
+ $templateMgr->assign([
+ 'pageComponent' => 'PageOJS',
+ 'breadcrumbs' => $breadcrumbs,
+ 'pageWidth' => TemplateManager::PAGE_WIDTH_FULL,
+ ]);
+ $templateMgr->display('/invitation/userInvitation.tpl');
+ }
+
+ /**
+ * Get current user user groups
+ * @param int $id
+ * @return array
+ */
+ private function getUserUserGroups(int $id): array
+ {
+ $output = [];
+ $userGroups = Repo::userGroup()->userUserGroups($id);
+ foreach ($userGroups as $userGroup) {
+ $output[] = [
+ 'id' => (int) $userGroup->getId(),
+ 'name' => $userGroup->getName(null),
+ 'abbrev' => $userGroup->getAbbrev(null),
+ 'roleId' => (int) $userGroup->getRoleId(),
+ 'showTitle' => (bool) $userGroup->getShowTitle(),
+ 'permitSelfRegistration' => (bool) $userGroup->getPermitSelfRegistration(),
+ 'permitMetadataEdit' => (bool) $userGroup->getPermitMetadataEdit(),
+ 'recommendOnly' => (bool) $userGroup->getRecommendOnly(),
+ ];
+ }
+ return $output;
+ }
}
diff --git a/pages/invitation/index.php b/pages/invitation/index.php
index cbe585b1269..d9d964fa01f 100644
--- a/pages/invitation/index.php
+++ b/pages/invitation/index.php
@@ -16,5 +16,6 @@
switch ($op) {
case 'decline':
case 'accept':
+ case 'invite':
return new PKP\pages\invitation\InvitationHandler();
}
diff --git a/schemas/submission.json b/schemas/submission.json
index 9eb11dca2ac..0dfac389b9c 100644
--- a/schemas/submission.json
+++ b/schemas/submission.json
@@ -265,6 +265,21 @@
"type": "boolean"
}
}
+ },
+ "availableEditorialDecisions": {
+ "description": "The editorial decisions that are available for this stage.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "label": {
+ "type": "string"
+ }
+ }
+ }
}
}
}
diff --git a/templates/frontend/pages/editorialHistory.tpl b/templates/frontend/pages/editorialHistory.tpl
index bf96d087ce4..694f441f190 100644
--- a/templates/frontend/pages/editorialHistory.tpl
+++ b/templates/frontend/pages/editorialHistory.tpl
@@ -18,32 +18,30 @@
{foreach from=$mastheadRoles item="mastheadRole"}
{if array_key_exists($mastheadRole->getId(), $mastheadUsers)}
{$mastheadRole->getLocalizedName()|escape}
-
+
{foreach from=$mastheadUsers[$mastheadRole->getId()] item="mastheadUser"}
-
-
- - {$mastheadUser['user']->getFullName()|escape}
- {if !empty($mastheadUser['user']->getLocalizedData('affiliation'))}
- - {$mastheadUser['user']->getLocalizedData('affiliation')|escape}
- {/if}
- {if $mastheadUser['user']->getData('orcid')}
-
- {if $mastheadUser['user']->getData('orcidAccessToken')}
+ {strip}
+
+ {foreach name="services" from=$mastheadUser['services'] item="service"}
+ {translate key="common.fromUntil" from=$service['dateStart'] until=$service['dateEnd']}
+ {if !$smarty.foreach.services.last}{translate key="common.commaListSeparator"}{/if}
+ {/foreach}
+
+
+ {$mastheadUser['user']->getFullName()|escape}
+ {if $mastheadUser['user']->getData('orcid') && $mastheadUser['user']->getData('orcidAccessToken')}
+
+ getFullName()|escape}">
{$orcidIcon}
- {/if}
-
- {$mastheadUser['user']->getData('orcid')|escape}
{/if}
- -
- {foreach from=$mastheadUser['services'] item="service"}
-
- - {translate key="common.fromUntil" from=$service['dateStart'] until=$service['dateEnd']}
-
- {/foreach}
-
-
+
+ {if !empty($mastheadUser['user']->getLocalizedData('affiliation'))}
+ {$mastheadUser['user']->getLocalizedData('affiliation')|escape}
+ {/if}
+ {/strip}
{/foreach}
diff --git a/templates/frontend/pages/editorialMasthead.tpl b/templates/frontend/pages/editorialMasthead.tpl
index 3d6c3541c53..8226a5c51b5 100644
--- a/templates/frontend/pages/editorialMasthead.tpl
+++ b/templates/frontend/pages/editorialMasthead.tpl
@@ -17,32 +17,31 @@
{foreach from=$mastheadRoles item="mastheadRole"}
{if array_key_exists($mastheadRole->getId(), $mastheadUsers)}
{$mastheadRole->getLocalizedName()|escape}
-
+
{foreach from=$mastheadUsers[$mastheadRole->getId()] item="mastheadUser"}
-
-
- - {$mastheadUser['user']->getFullName()|escape}
- {if !empty($mastheadUser['user']->getLocalizedData('affiliation'))}
- - {$mastheadUser['user']->getLocalizedData('affiliation')|escape}
- {/if}
- - {$mastheadUser['dateStart']}
-
- {if $mastheadUser['user']->getData('orcid')}
-
- {if $mastheadUser['user']->getData('orcidAccessToken')}
- {$orcidIcon}
+ {strip}
+ {translate key="common.fromUntil" from=$mastheadUser['dateStart'] until=""}
+
+ {$mastheadUser['user']->getFullName()|escape}
+ {if $mastheadUser['user']->getData('orcid') && $mastheadUser['user']->getData('orcidAccessToken')}
+
+ getFullName()|escape}">
+ {$orcidIcon}
+
+
{/if}
-
- {$mastheadUser['user']->getData('orcid')|escape}
-
- {/if}
+ {if !empty($mastheadUser['user']->getLocalizedData('affiliation'))}
+ {$mastheadUser['user']->getLocalizedData('affiliation')|escape}
+ {/if}
+ {/strip}
{/foreach}
{/if}
{/foreach}
-
+
{capture assign=editorialHistoryUrl}{url page="about" op="editorialHistory" router=\PKP\core\PKPApplication::ROUTE_PAGE}{/capture}
{translate key="about.editorialMasthead.linkToEditorialHistory" url=$editorialHistoryUrl}
@@ -51,25 +50,24 @@
{if !empty($reviewers)}
{translate key="common.editorialMasthead.peerReviewers"}
{translate key="common.editorialMasthead.peerReviewers.description" year=$previousYear}
-
+
{foreach from=$reviewers item="reviewer"}
-
-
- - {$reviewer->getFullName()|escape}
- {if !empty($reviewer->getLocalizedData('affiliation'))}
- - {$reviewer->getLocalizedData('affiliation')|escape}
- {/if}
-
- {if $reviewer->getData('orcid')}
-
- {if $reviewer->getData('orcidAccessToken')}
- {$orcidIcon}
+ {strip}
+
+ {$reviewer->getFullName()|escape}
+ {if $reviewer->getData('orcid') && $reviewer->getData('orcidAccessToken')}
+
+ getFullName()|escape}">
+ {$orcidIcon}
+
+
{/if}
-
- {$reviewer->getData('orcid')|escape}
-
- {/if}
+ {if !empty($reviewer->getLocalizedData('affiliation'))}
+ {$reviewer->getLocalizedData('affiliation')|escape}
+ {/if}
+ {/strip}
{/foreach}
diff --git a/templates/invitation/acceptInvitation.tpl b/templates/invitation/acceptInvitation.tpl
new file mode 100644
index 00000000000..b2999ad38fc
--- /dev/null
+++ b/templates/invitation/acceptInvitation.tpl
@@ -0,0 +1,23 @@
+
+{/block}
diff --git a/templates/invitation/userInvitation.tpl b/templates/invitation/userInvitation.tpl
new file mode 100644
index 00000000000..2216ddff415
--- /dev/null
+++ b/templates/invitation/userInvitation.tpl
@@ -0,0 +1,25 @@
+
+{/block}
diff --git a/templates/management/access.tpl b/templates/management/access.tpl
index a39482b6569..9d10a03e30a 100644
--- a/templates/management/access.tpl
+++ b/templates/management/access.tpl
@@ -18,6 +18,7 @@
+
{include file="management/accessUsers.tpl"}
diff --git a/xml/onixFilter.xsl b/xml/onixFilter.xsl
index 3da7ef5a193..2ad5a3dd44e 100644
--- a/xml/onixFilter.xsl
+++ b/xml/onixFilter.xsl
@@ -3,59 +3,74 @@
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ()
+
+
+
+
+
+
+
+
+
+
-
-
-
- ()
-
-
-
-
-
-
-