diff --git a/api/v1/stats/sushi/PKPStatsSushiController.php b/api/v1/stats/sushi/PKPStatsSushiController.php index c2c9e6961b0..8c9d08dcd1b 100644 --- a/api/v1/stats/sushi/PKPStatsSushiController.php +++ b/api/v1/stats/sushi/PKPStatsSushiController.php @@ -26,6 +26,7 @@ use Illuminate\Support\Facades\Route; use PKP\core\PKPBaseController; use PKP\core\PKPRequest; +use PKP\core\PKPRoutingProvider; use PKP\security\authorization\ContextRequiredPolicy; use PKP\security\authorization\PolicySet; use PKP\security\authorization\RoleBasedHandlerOperationPolicy; @@ -33,6 +34,8 @@ use PKP\security\Role; use PKP\sushi\CounterR5Report; use PKP\sushi\SushiException; +use PKP\validation\ValidatorFactory; +use Symfony\Component\HttpFoundation\StreamedResponse; class PKPStatsSushiController extends PKPBaseController { @@ -243,7 +246,7 @@ protected function getReportList(): array * COUNTER 'Platform Usage' [PR_P1]. * A customizable report summarizing activity across the Platform (journal, press, or server). */ - public function getReportsPR(Request $illuminateRequest): JsonResponse + public function getReportsPR(Request $illuminateRequest): JsonResponse|StreamedResponse { return $this->getReportResponse(new PR(), $illuminateRequest); } @@ -252,17 +255,98 @@ public function getReportsPR(Request $illuminateRequest): JsonResponse * COUNTER 'Platform Master Report' [PR]. * This is a Standard View of the Platform Master Report that presents usage for the overall Platform broken down by Metric_Type */ - public function getReportsPR1(Request $illuminateRequest): JsonResponse + public function getReportsPR1(Request $illuminateRequest): JsonResponse|StreamedResponse { return $this->getReportResponse(new PR_P1(), $illuminateRequest); } + /** Validate user input for TSV reports */ + protected function _validateUserInput(CounterR5Report $report, array $params): array + { + $request = $this->getRequest(); + $context = $request->getContext(); + $earliestDate = CounterR5Report::getEarliestDate(); + $lastDate = CounterR5Report::getLastDate(); + $submissionIds = Repo::submission()->getCollector()->filterByContextIds([$context->getId()])->getIds()->implode(','); + + $rules = [ + 'begin_date' => [ + 'regex:/^\d{4}-\d{2}(-\d{2})?$/', + 'after_or_equal:' . $earliestDate, + 'before_or_equal:end_date', + ], + 'end_date' => [ + 'regex:/^\d{4}-\d{2}(-\d{2})?$/', + 'before_or_equal:' . $lastDate, + 'after_or_equal:begin_date', + ], + 'item_id' => [ + // TO-ASK: shell this rather be just validation for positive integer? + 'in:' . $submissionIds, + ], + 'yop' => [ + 'regex:/^\d{4}((\||-)\d{4})*$/', + ], + ]; + $reportId = $report->getID(); + if (in_array($reportId, ['PR', 'TR', 'IR'])) { + $rules['metric_type'] = ['required']; + } + + $errors = []; + $validator = ValidatorFactory::make( + $params, + $rules, + [ + 'begin_date.regex' => __( + 'manager.statistics.counterR5Report.settings.wrongDateFormat' + ), + 'end_date.regex' => __( + 'manager.statistics.counterR5Report.settings.wrongDateFormat' + ), + 'begin_date.after_or_equal' => __( + 'stats.dateRange.invalidStartDateMin' + ), + 'end_date.before_or_equal' => __( + 'stats.dateRange.invalidEndDateMax' + ), + 'begin_date.before_or_equal' => __( + 'stats.dateRange.invalidDateRange' + ), + 'end_date.after_or_equal' => __( + 'stats.dateRange.invalidDateRange' + ), + 'item_id.*' => __( + 'manager.statistics.counterR5Report.settings.wrongItemId' + ), + 'yop.regex' => __( + 'manager.statistics.counterR5Report.settings.wrongYOPFormat' + ), + ] + ); + + if ($validator->fails()) { + $errors = $validator->errors()->getMessages(); + } + + return $errors; + } + /** * Get the requested report */ - protected function getReportResponse(CounterR5Report $report, Request $illuminateRequest): JsonResponse + protected function getReportResponse(CounterR5Report $report, Request $illuminateRequest): JsonResponse|StreamedResponse { $params = $illuminateRequest->query(); + //$responseTSV = str_contains($illuminateRequest->getHeaderLine('Accept'), PKPRoutingProvider::RESPONSE_TSV['mime']) ? true : false; + $responseTSV = $illuminateRequest->accepts(PKPRoutingProvider::RESPONSE_TSV['mime']); + + if ($responseTSV) { + $errors = $this->_validateUserInput($report, $params); + if (!empty($errors)) { + return response()->json($errors, 400); + } + } try { $report->processReportParams($this->getRequest(), $params); @@ -270,6 +354,27 @@ protected function getReportResponse(CounterR5Report $report, Request $illuminat return response()->json($e->getResponseData(), $e->getHttpStatusCode()); } + if ($responseTSV) { + $reportHeader = $report->getTSVReportHeader(); + $reportColumnNames = $report->getTSVColumnNames(); + $reportItems = $report->getTSVReportItems(); + // consider 3030 error (no usage available) + $key = array_search('3030', array_column($report->warnings, 'Code')); + if ($key !== false) { + $error = $report->warnings[$key]['Code'] . ':' . $report->warnings[$key]['Message'] . '(' . $report->warnings[$key]['Data'] . ')'; + foreach ($reportHeader as &$headerRow) { + if (in_array('Exceptions', $headerRow)) { + $headerRow[1] = + $headerRow[1] == '' ? + $error : + $headerRow[1] . ';' . $error; + } + } + } + $report = array_merge($reportHeader, [['']], $reportColumnNames, $reportItems); + return response()->withFile($report, [], count($reportItems)); + } + $reportHeader = $report->getReportHeader(); $reportItems = $report->getReportItems(); diff --git a/classes/components/forms/counter/PKPCounterReportForm.php b/classes/components/forms/counter/PKPCounterReportForm.php new file mode 100644 index 00000000000..03e67208131 --- /dev/null +++ b/classes/components/forms/counter/PKPCounterReportForm.php @@ -0,0 +1,66 @@ +action = $action; + $this->locales = $locales; + + $this->addPage(['id' => 'default', 'submitButton' => ['label' => __('common.download')]]); + $this->addGroup(['id' => 'default', 'pageId' => 'default']); + + $this->setReportFields(); + } + + public function getConfig() + { + $config = parent::getConfig(); + $config['reportFields'] = array_map(function ($reportFields) { + return array_map(function ($reportField) { + $field = $this->getFieldConfig($reportField); + $field['groupId'] = 'default'; + return $field; + }, $reportFields); + }, $this->reportFields); + + return $config; + } +} diff --git a/classes/components/forms/dashboard/SubmissionFilters.php b/classes/components/forms/dashboard/PKPSubmissionFilters.php similarity index 89% rename from classes/components/forms/dashboard/SubmissionFilters.php rename to classes/components/forms/dashboard/PKPSubmissionFilters.php index 92f7370b19a..0911c6a2b35 100644 --- a/classes/components/forms/dashboard/SubmissionFilters.php +++ b/classes/components/forms/dashboard/PKPSubmissionFilters.php @@ -15,7 +15,6 @@ namespace PKP\components\forms\dashboard; -use APP\components\forms\FieldSelectIssues; use APP\core\Application; use APP\facades\Repo; use Illuminate\Support\LazyCollection; @@ -27,7 +26,7 @@ use PKP\context\Context; use PKP\security\Role; -class SubmissionFilters extends FormComponent +class PKPSubmissionFilters extends FormComponent { /** * The maximum number of options in a field @@ -49,7 +48,6 @@ public function __construct( ->addGroup(['id' => 'default', 'pageId' => 'default']) ->addSectionFields() ->addAssignedTo() - ->addIssues() ->addCategories() ->addDaysSinceLastActivity() ; @@ -119,17 +117,6 @@ protected function addAssignedTo(): self ])); } - protected function addIssues(): self - { - $request = Application::get()->getRequest(); - - return $this->addField(new FieldSelectIssues('issueIds', [ - 'groupId' => 'default', - 'label' => __('issue.issues'), - 'value' => [], - 'apiUrl' => $request->getDispatcher()->url($request, Application::ROUTE_API, $request->getContext()->getPath(), 'issues'), - ])); - } protected function addCategories(): self { diff --git a/classes/components/forms/publication/ContributorForm.php b/classes/components/forms/publication/ContributorForm.php index f608d4b9e03..51212937f6e 100644 --- a/classes/components/forms/publication/ContributorForm.php +++ b/classes/components/forms/publication/ContributorForm.php @@ -119,6 +119,7 @@ public function __construct(string $action, array $locales, ?Submission $submiss ->addField(new FieldText('affiliation', [ 'label' => __('user.affiliation'), 'isMultilingual' => true, + 'size' => 'large', ])); if ($authorUserGroupsOptions->count() > 1) { diff --git a/classes/components/listPanels/PKPCounterReportsListPanel.php b/classes/components/listPanels/PKPCounterReportsListPanel.php new file mode 100644 index 00000000000..0063056af1e --- /dev/null +++ b/classes/components/listPanels/PKPCounterReportsListPanel.php @@ -0,0 +1,52 @@ + $this->apiUrl, + 'form' => $this->form->getConfig(), + 'usagePossible' => $lastDate > $earliestDate, + ] + ); + return $config; + } +} diff --git a/classes/services/queryBuilders/PKPStatsSushiQueryBuilder.php b/classes/services/queryBuilders/PKPStatsSushiQueryBuilder.php index 8bc79818fec..61cc5a47ac5 100644 --- a/classes/services/queryBuilders/PKPStatsSushiQueryBuilder.php +++ b/classes/services/queryBuilders/PKPStatsSushiQueryBuilder.php @@ -76,7 +76,10 @@ public function getSum(array $groupBy = []): Builder $q->leftJoin('publications as p', function ($q) { $q->on('p.submission_id', '=', 'm.submission_id') ->whereIn('p.publication_id', function ($q) { - $q->selectRaw('MIN(p2.publication_id)')->from('publications as p2')->where('p2.status', Submission::STATUS_PUBLISHED); + $q->selectRaw('MIN(p2.publication_id)') + ->from('publications as p2') + ->where('p2.status', Submission::STATUS_PUBLISHED) + ->where('p2.submission_id', '=', DB::raw('m.submission_id')); }); }); } @@ -123,7 +126,10 @@ protected function _getObject(): Builder $q->leftJoin('publications as p', function ($q) { $q->on('p.submission_id', '=', 'm.submission_id') ->whereIn('p.publication_id', function ($q) { - $q->selectRaw('MIN(p2.publication_id)')->from('publications as p2')->where('p2.status', Submission::STATUS_PUBLISHED); + $q->selectRaw('MIN(p2.publication_id)') + ->from('publications as p2') + ->where('p2.status', Submission::STATUS_PUBLISHED) + ->where('p2.submission_id', '=', DB::raw('m.submission_id')); }); }); foreach ($this->yearsOfPublication as $yop) { diff --git a/classes/sushi/CounterR5Report.php b/classes/sushi/CounterR5Report.php index c7c02aa4a48..4a5dd35835f 100644 --- a/classes/sushi/CounterR5Report.php +++ b/classes/sushi/CounterR5Report.php @@ -17,8 +17,14 @@ namespace PKP\sushi; +use APP\core\Application; use APP\facades\Repo; +use DateInterval; +use DatePeriod; use DateTime; +use Exception; +use PKP\components\forms\FieldSelect; +use PKP\components\forms\FieldText; use PKP\context\Context; abstract class CounterR5Report @@ -171,11 +177,16 @@ public function setAttributes(array $attributes): void } } - /** - * Get report items - */ + /** Get report items */ abstract public function getReportItems(): array; + /** Get report items prepared for TSV report */ + abstract public function getTSVReportItems(): array; + + /** Get TSV report column names */ + abstract public function getTSVColumnNames(): array; + + /** Add a warning */ protected function addWarning(array $exception): void { $this->warnings[] = $exception; @@ -292,25 +303,45 @@ protected function checkCustomerId($params): void } /** - * Validate the date parameters (begin_date, end_date) - * - * @throws SushiException + * Get the first month the usage data is available for COUNTER R5 reports. + * It is either: + * the next month of the COUNTER R5 start, or + * this journal's first publication date. */ - protected function checkDate($params): void + public static function getEarliestDate(): string { - // get the first month the usage data is available for COUNTER R5, it is either: - // the next month of the COUNTER R5 start, or - // this journal's first publication date. + $context = Application::get()->getRequest()->getContext(); $statsService = app()->get('sushiStats'); $counterR5StartDate = $statsService->getEarliestDate(); $firstDatePublished = Repo::publication()->getDateBoundaries( Repo::publication() ->getCollector() - ->filterByContextIds([$this->context->getId()]) + ->filterByContextIds([$context->getId()]) )->min_date_published; $earliestDate = strtotime($firstDatePublished) > strtotime($counterR5StartDate) ? $firstDatePublished : $counterR5StartDate; $earliestDate = date('Y-m-01', strtotime($earliestDate . ' + 1 months')); - $lastDate = date('Y-m-d', strtotime('last day of previous month')); // get the last month in the DB table + return $earliestDate; + } + + /** + * Get the last possible date COUNTER R5 reports could exist for. + * This is the last day of the previous month, + * because the all stats for the previous month should be already compiled. + */ + public static function getLastDate(): string + { + return date('Y-m-d', strtotime('last day of previous month')); + } + + /** + * Validate the date parameters (begin_date, end_date) + * + * @throws SushiException + */ + protected function checkDate($params): void + { + $earliestDate = self::getEarliestDate(); + $lastDate = self::getLastDate(); $beginDate = $params['begin_date']; $endDate = $params['end_date']; @@ -531,6 +562,78 @@ public function getReportHeader(): array return $reportHeader; } + /** Get report header for TSV reports */ + public function getTSVReportHeader(): array + { + $institutionIds = []; + if (isset($this->institutionIds)) { + foreach ($this->institutionIds as $institutionId) { + if ($institutionId['Type'] == 'Proprietary') { + $institutionIds[] = $institutionId['Value']; + } else { + $institutionIds[] = $institutionId['Type'] . ':' . $institutionId['Value']; + } + } + } + $reportHeaderInstitutionId = !empty($institutionIds) ? implode(';', $institutionIds) : ''; + $reportHeaderMetricTypes = $beginDate = $endDate = ''; + $reportHeaderFilters = $reportHeaderAttributes = []; + foreach ($this->filters as $filter) { + switch ($filter['Name']) { + case ('Metric_Type'): + $reportHeaderMetricTypes = implode(';', explode('|', $filter['Value'])); + break; + case ('Begin_Date'): + $beginDate = $filter['Name'] . '=' . $filter['Value']; + break; + case ('End_Date'): + $endDate = $filter['Name'] . '=' . $filter['Value']; + break; + default: + $reportHeaderFilters[] = $filter['Name'] . '=' . $filter['Value']; + } + } + foreach ($this->attributes as $attribute) { + if ($attribute['Name'] == 'granularity') { + $excludeMonthlyDetails = $attribute['Value'] == 'Month' ? 'False' : 'True'; + $reportHeaderAttributes[] = 'Exclude_Monthly_Details' . '=' . $excludeMonthlyDetails; + } else { + $reportHeaderAttributes[] = $attribute['Name'] . '=' . $attribute['Value']; + } + } + + $exceptions = []; + foreach ($this->warnings as $warning) { + $exceptions[] = $warning['Code'] . ':' . $warning['Message'] . '(' . $warning['Data'] . ')'; + } + + $reportHeader = [ + ['Report_Name', $this->getName()], + ['Report_ID', $this->getID()], + ['Release', $this->getRelease()], + ['Institution_Name', $this->institutionName], + ['Institution_ID', $reportHeaderInstitutionId], + ['Metric_Types', $reportHeaderMetricTypes], + ['Report_Filters', implode(';', $reportHeaderFilters)], + ['Report_Attributes', implode(';', $reportHeaderAttributes)], + ['Exceptions', implode(';', $exceptions)], + ['Reporting_Period', $beginDate . ';' . $endDate], + ['Created', date('Y-m-d\TH:i:s\Z', time())], + ['Created_By', $this->platformName], + ]; + return $reportHeader; + } + + /** Get monthly period */ + protected function getMonthlyDatePeriod(): DatePeriod + { + // every month for the given period needs to be considered + $start = new DateTime($this->beginDate); + $end = new DateTime($this->endDate); + $interval = DateInterval::createFromDateString('1 month'); + return new DatePeriod($start, $interval, $end); + } + /** * Validate date, check if the date is a valid date and in requested format */ @@ -539,4 +642,51 @@ protected function validateDate(string $date, string $format = 'Y-m-d'): bool $d = DateTime::createFromFormat($format, $date); return $d && $d->format($format) === $date; } + + /** + * Get report form fields common to all reports + */ + public static function getCommonReportSettingsFormFields(): array + { + $context = Application::get()->getRequest()->getContext(); + $institutions = Repo::institution()->getCollector() + ->filterByContextIds([$context->getId()]) + ->getMany(); + + $institutionOptions = [['value' => '0', 'label' => 'The World']]; + foreach ($institutions as $institution) { + $institutionOptions[] = ['value' => $institution->getId(), 'label' => $institution->getLocalizedName()]; + } + + $earliestDate = self::getEarliestDate(); + $lastDate = self::getLastDate(); + + return [ + new FieldText('begin_date', [ + 'label' => __('manager.statistics.counterR5Report.settings.startDate'), + 'description' => __('manager.statistics.counterR5Report.settings.date.startDate.description', ['earliestDate' => $earliestDate]), + 'size' => 'small', + 'isMultilingual' => false, + 'isRequired' => true, + 'value' => $earliestDate, + 'groupId' => 'default', + ]), + new FieldText('end_date', [ + 'label' => __('manager.statistics.counterR5Report.settings.endDate'), + 'description' => __('manager.statistics.counterR5Report.settings.date.endDate.description', ['lastDate' => $lastDate]), + 'size' => 'small', + 'isMultilingual' => false, + 'isRequired' => true, + 'value' => $lastDate, + 'groupId' => 'default', + ]), + new FieldSelect('customer_id', [ + 'label' => __('manager.statistics.counterR5Report.settings.customerId'), + 'options' => $institutionOptions, + 'value' => '0', + 'isRequired' => true, + 'groupId' => 'default', + ]), + ]; + } } diff --git a/classes/template/PKPTemplateManager.php b/classes/template/PKPTemplateManager.php index d21963db271..d458dfebc8a 100644 --- a/classes/template/PKPTemplateManager.php +++ b/classes/template/PKPTemplateManager.php @@ -1004,7 +1004,7 @@ public function setupBackendPage() }); $viewsData['newSubmission'] = [ - 'name' => __('author.submit.startHereTitle'), + 'name' => __('dashboard.startNewSubmission'), 'url' => $router->url($request, null, 'submission') ]; @@ -1052,7 +1052,7 @@ public function setupBackendPage() }); $viewsData['newSubmission'] = [ - 'name' => __('author.submit.startHereTitle'), + 'name' => __('dashboard.startNewSubmission'), 'url' => $router->url($request, null, 'submission') ]; @@ -1166,6 +1166,11 @@ public function setupBackendPage() 'name' => __('manager.users'), 'url' => $router->url($request, null, 'stats', 'users', ['users']), 'isCurrent' => $router->getRequestedPage($request) === 'stats' && $router->getRequestedOp($request) === 'users', + ], + 'counterR5' => [ + 'name' => __('manager.statistics.counterR5'), + 'url' => $router->url($request, null, 'stats', 'counterR5', ['counterR5']), + 'isCurrent' => $router->getRequestedPage($request) === 'stats' && $router->getRequestedOp($request) === 'counterR5', ] ] ]; diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index 30e960bc97e..8aae1474013 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -21,6 +21,7 @@ use PKP\services\PKPSchemaService; use PKP\stageAssignment\StageAssignment; use PKP\user\User; +use PKP\userGroup\relationships\UserUserGroup; use PKP\workflow\WorkflowStageDAO; use Submission; @@ -156,7 +157,7 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat $userGroups = Repo::userGroup()->userUserGroups($user->getId(), $this->context->getId()); $output[$prop] = []; foreach ($userGroups as $userGroup) { - $output[$prop][] = [ + $output[$prop][] = [ 'id' => (int) $userGroup->getId(), 'name' => $userGroup->getName(null), 'abbrev' => $userGroup->getAbbrev(null), @@ -165,6 +166,10 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat 'permitSelfRegistration' => (bool) $userGroup->getPermitSelfRegistration(), 'permitMetadataEdit' => (bool) $userGroup->getPermitMetadataEdit(), 'recommendOnly' => (bool) $userGroup->getRecommendOnly(), + 'dateStart' => UserUserGroup::withUserId($user->getId()) + ->withActive() + ->withUserGroupId($userGroup->getId()) + ->pluck('date_start')->first() ]; } } diff --git a/cypress/support/commands_new_workflow.js b/cypress/support/commands_new_workflow.js index 7dbdf2ee110..ee09a530494 100644 --- a/cypress/support/commands_new_workflow.js +++ b/cypress/support/commands_new_workflow.js @@ -182,19 +182,30 @@ Cypress.Commands.add('openReviewAssignment', (familyName) => { }); -Cypress.Commands.add('openWorkflowMenu', (name) => { - cy.get(`[data-cy="active-modal"] nav a:contains("${name}")`).click(); +Cypress.Commands.add('openWorkflowMenu', (name, subitem = null) => { + if(subitem) { + cy.get(`[data-cy="active-modal"] nav a:contains("${name}")`).contains(subitem).click() + } else { + cy.get(`[data-cy="active-modal"] nav a:contains("${name}")`).click(); + + } cy.get('[data-cy="active-modal"] h2').contains(name); }); +Cypress.Commands.add('openReviewAssignment', (familyName) => { + cy.contains('table tr', familyName).within(() => { + cy.get('button').click() + }) +}); -Cypress.Commands.add('findSubmissionAsEditor', (username, password, familyName, context) => { +Cypress.Commands.add('findSubmissionAsEditor', (username, password, familyName, context = null, viewName = null) => { context = context || 'publicknowledge'; + viewName = viewName || 'Active submissions'; cy.login(username, password, context); - cy.get('nav').contains('Active submissions').click(); + cy.get('nav').contains(viewName).click(); cy.contains('table tr', familyName).within(() => { cy.get('button').contains('View').click() }) @@ -465,7 +476,7 @@ Cypress.Commands.add('submissionIsDeclined', () => { }); Cypress.Commands.add('isActiveStageTab', (stageName) => { - cy.get('[data-cy="active-modal"] nav .bg-selection-dark').contains(stageName); + cy.get('[data-cy="active-modal"] h2').contains(stageName); }); /** diff --git a/locale/en/dashboard.po b/locale/en/dashboard.po index be60b2d6b35..5c4f3d7e0fc 100644 --- a/locale/en/dashboard.po +++ b/locale/en/dashboard.po @@ -316,3 +316,9 @@ msgstr "All editorial recommendations have been received, and a decision is requ msgid "dashboard.recommendOnly.onlyAllowedToRecommend" msgstr "Only allowed to recommend an editorial decision" + +msgid "dashboard.workflow.reviewRoundN" +msgstr "Review Round {$number}" + +msgid "dashboard.startNewSubmission" +msgstr "Start a New Submission" diff --git a/locale/en/manager.po b/locale/en/manager.po index 54e1e87cfb6..833ffff4e2f 100644 --- a/locale/en/manager.po +++ b/locale/en/manager.po @@ -849,6 +849,66 @@ msgstr "Whether or not to restrict access to the API endpoints for COUNTER SUSHI msgid "manager.settings.statistics.publicSushiApi.public" msgstr "Make the COUNTER SUSHI statistics publicly available" +msgid "manager.statistics.counterR5" +msgstr "Counter R5" + +msgid "manager.statistics.counterR5Reports" +msgstr "Counter R5 Reports" + +msgid "manager.statistics.counterR5Reports.description" +msgstr "See COUNTER 5.0.3 documentation for more information about each report." + +msgid "manager.statistics.counterR5Reports.usageNotPossible" +msgstr "There are no COUNTER R5 usage statistics available yet." + +msgid "manager.statistics.counterR5Report.settings" +msgstr "Report Settings" + +msgid "manager.statistics.counterR5Report.settings.startDate" +msgstr "Start Date" + +msgid "manager.statistics.counterR5Report.settings.date.startDate.description" +msgstr "Date should be in format YYYY-MM-DD or YYYY-MM. Earliest possible date is {$earliestDate}." + +msgid "manager.statistics.counterR5Report.settings.endDate" +msgstr "End Date" + +msgid "manager.statistics.counterR5Report.settings.date.endDate.description" +msgstr "Date should be in format YYYY-MM-DD or YYYY-MM. Last possible date is {$lastDate}." + +msgid "manager.statistics.counterR5Report.settings.wrongDateFormat" +msgstr "The date format is not valid." + +msgid "manager.statistics.counterR5Report.settings.customerId" +msgstr "Customer ID" + +msgid "manager.statistics.counterR5Report.settings.metricType" +msgstr "Metric Type" + +msgid "manager.statistics.counterR5Report.settings.attributesToShow" +msgstr "Attributes To Show" + +msgid "manager.statistics.counterR5Report.settings.yop" +msgstr "Year Of Publication" + +msgid "manager.statistics.counterR5Report.settings.date.yop.description" +msgstr "A list or range of years of publication to return in response in format of yyyy|yyyy|yyyy-yyyy." + +msgid "manager.statistics.counterR5Report.settings.wrongYOPFormat" +msgstr "YOP format is not valid." + +msgid "manager.statistics.counterR5Report.settings.itemId" +msgstr "Submission ID" + +msgid "manager.statistics.counterR5Report.settings.wrongItemId" +msgstr "The submission ID does not exist." + +msgid "manager.statistics.counterR5Report.settings.includeParentDetails" +msgstr "Include Parent Details" + +msgid "manager.statistics.counterR5Report.settings.excludeMonthlyDetails" +msgstr "Exclude Monthly Details" + msgid "manager.statistics.reports" msgstr "Reports" diff --git a/pages/dashboard/DashboardHandlerNext.php b/pages/dashboard/PKPDashboardHandlerNext.php similarity index 94% rename from pages/dashboard/DashboardHandlerNext.php rename to pages/dashboard/PKPDashboardHandlerNext.php index ab6e59e2898..9d6b3552047 100644 --- a/pages/dashboard/DashboardHandlerNext.php +++ b/pages/dashboard/PKPDashboardHandlerNext.php @@ -20,7 +20,6 @@ use APP\facades\Repo; use APP\handler\Handler; use APP\template\TemplateManager; -use PKP\components\forms\dashboard\SubmissionFilters; use PKP\controllers\grid\users\reviewer\PKPReviewerGridHandler; use PKP\core\JSONMessage; use PKP\core\PKPApplication; @@ -35,6 +34,7 @@ use PKP\submissionFile\SubmissionFile; use PKP\components\forms\publication\ContributorForm; use PKP\plugins\PluginRegistry; + use PKP\notification\Notification; define('SUBMISSIONS_LIST_ACTIVE', 'active'); @@ -50,7 +50,7 @@ enum DashboardPage: string } -class DashboardHandlerNext extends Handler +abstract class PKPDashboardHandlerNext extends Handler { /** @copydoc PKPHandler::_isBackendPage */ public $_isBackendPage = true; @@ -126,22 +126,7 @@ public function index($args, $request) $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES); - $sections = Repo::section() - ->getCollector() - ->filterByContextIds([$context->getId()]) - ->getMany(); - - $categories = Repo::category() - ->getCollector() - ->filterByContextIds([$context->getId()]) - ->getMany(); - - $filtersForm = new SubmissionFilters( - $context, - $userRoles, - $sections, - $categories - ); + $filtersForm = $this->getSubmissionFiltersForm($userRoles, $context); // ContributorsForm $contributorForm = new ContributorForm( @@ -152,8 +137,6 @@ public function index($args, $request) ); - - $selectRevisionDecisionForm = new \PKP\components\forms\decision\SelectRevisionDecisionForm(); $selectRevisionRecommendationForm = new \PKP\components\forms\decision\SelectRevisionRecommendationForm(); @@ -167,8 +150,6 @@ public function index($args, $request) } } - // OJS specific, might need to be adjusted for OMP/OPS - $paymentManager = Application::get()->getPaymentManager($context); $templateMgr->setState([ 'pageInitConfig' => [ @@ -183,20 +164,17 @@ public function index($args, $request) 'publicationSettings' => [ 'supportsCitations' => !!$context->getData('citations'), 'identifiersEnabled' => $identifiersEnabled, - 'submissionPaymentsEnabled' => $paymentManager->publicationEnabled() ] ] ]); $templateMgr->assign([ - 'pageComponent' => 'PageOJS', + 'pageComponent' => 'Page', 'pageTitle' => __('navigation.submissions'), 'pageWidth' => TemplateManager::PAGE_WIDTH_FULL, ]); - class_exists(\APP\components\forms\publication\AssignToIssueForm::class); // Force define of FORM_ASSIGN_TO_ISSUE - $templateMgr->setConstants([ 'STAGE_STATUS_SUBMISSION_UNASSIGNED' => Repo::submission()::STAGE_STATUS_SUBMISSION_UNASSIGNED, 'REVIEW_ASSIGNMENT_STATUS_DECLINED' => ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_DECLINED, @@ -253,7 +231,6 @@ class_exists(\APP\components\forms\publication\AssignToIssueForm::class); // For 'DECISION_RECOMMEND_PENDING_REVISIONS' => Decision::RECOMMEND_PENDING_REVISIONS, 'DECISION_RECOMMEND_RESUBMIT' => Decision::RECOMMEND_RESUBMIT, - 'SUBMISSION_FILE_SUBMISSION' => SubmissionFile::SUBMISSION_FILE_SUBMISSION, 'SUBMISSION_FILE_REVIEW_FILE' => SubmissionFile::SUBMISSION_FILE_REVIEW_FILE, 'SUBMISSION_FILE_REVIEW_REVISION' => SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION, @@ -263,7 +240,6 @@ class_exists(\APP\components\forms\publication\AssignToIssueForm::class); // For 'SUBMISSION_FILE_PRODUCTION_READY' => SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY, 'SUBMISSION_FILE_PROOF' => SubmissionFile::SUBMISSION_FILE_PROOF, 'SUBMISSION_FILE_JATS' => SubmissionFile::SUBMISSION_FILE_JATS, - 'FORM_ASSIGN_TO_ISSUE' => FORM_ASSIGN_TO_ISSUE, 'FORM_PUBLISH' => PublishForm::FORM_PUBLISH, 'REVIEWER_SELECT_ADVANCED_SEARCH' => PKPReviewerGridHandler::REVIEWER_SELECT_ADVANCED_SEARCH, @@ -280,9 +256,14 @@ class_exists(\APP\components\forms\publication\AssignToIssueForm::class); // For 'NOTIFICATION_TYPE_VISIT_CATALOG' => Notification::NOTIFICATION_TYPE_VISIT_CATALOG, 'NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER' => Notification::NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER, - 'NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS' => Notification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS + 'NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS' => Notification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS, + + // OMP specific constants ]); + $this->setupIndex($request); + + $templateMgr->display('dashboard/editors.tpl'); } @@ -413,4 +394,22 @@ public function __construct( } }; } + + /** + * Placeholder method to be overridden by apps in order to add + * app-specific data to the template + * + * @param Request $request + */ + public function setupIndex($request) + { + } + + + /** + */ + abstract protected function getSubmissionFiltersForm($userRoles, $context); + + + } diff --git a/pages/stats/PKPStatsHandler.php b/pages/stats/PKPStatsHandler.php index 897d27d5f66..69d51fa5f13 100644 --- a/pages/stats/PKPStatsHandler.php +++ b/pages/stats/PKPStatsHandler.php @@ -27,6 +27,7 @@ use PKP\security\authorization\ContextAccessPolicy; use PKP\security\Role; use PKP\statistics\PKPStatisticsHelper; +use PKP\sushi\CounterR5Report; class PKPStatsHandler extends Handler { @@ -41,7 +42,7 @@ public function __construct() parent::__construct(); $this->addRoleAssignment( [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR], - ['editorial', 'publications', 'context', 'users', 'reports'] + ['editorial', 'publications', 'context', 'users', 'reports', 'counterR5'] ); } @@ -430,6 +431,48 @@ public function context($args, $request) $templateMgr->display('stats/context.tpl'); } + /** + * Display list of available COUNTER R5 reports + */ + public function counterR5(array $args, Request $request): void + { + $templateMgr = TemplateManager::getManager($request); + $this->setupTemplate($request); + + $apiUrl = $request->getDispatcher()->url($request, PKPApplication::ROUTE_API, $request->getContext()->getPath(), 'stats/sushi'); + + $context = $request->getContext(); + $locales = $context->getSupportedFormLocaleNames(); + $locales = array_map(fn (string $locale, string $name) => ['key' => $locale, 'label' => $name], array_keys($locales), $locales); + + $counterReportForm = new \APP\components\forms\counter\CounterReportForm($apiUrl, $locales); + + $counterReportsListPanel = new \PKP\components\listPanels\PKPCounterReportsListPanel( + 'counterReportsListPanel', + __('manager.statistics.counterR5Reports'), + [ + 'apiUrl' => $apiUrl, + 'form' => $counterReportForm, + ] + ); + + $earliestDate = CounterR5Report::getEarliestDate(); + $lastDate = CounterR5Report::getLastDate(); + + $templateMgr->setState([ + 'pageInitConfig' => [ + $counterReportsListPanel->id => $counterReportsListPanel->getConfig(), + 'usageNotPossible' => $lastDate <= $earliestDate, + ], + ]); + $templateMgr->assign([ + 'pageComponent' => 'Page', + 'pageTitle' => __('manager.statistics.counterR5Reports'), + 'pageWidth' => TemplateManager::PAGE_WIDTH_FULL, + ]); + $templateMgr->display('stats/counterReports.tpl'); + } + /** * Display users stats * diff --git a/templates/stats/counterReports.tpl b/templates/stats/counterReports.tpl new file mode 100644 index 00000000000..0509d82d2f6 --- /dev/null +++ b/templates/stats/counterReports.tpl @@ -0,0 +1,14 @@ +{** + * templates/stats/counterReports.tpl + * + * Copyright (c) 2024 Simon Fraser University + * Copyright (c) 2024 John Willinsky + * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. + * + * @brief Set up and download COUNTER R5 TSV reports + *} +{extends file="layouts/backend.tpl"} + +{block name="page"} + +{/block} diff --git a/tests/jobs/email/ReviewReminderTest.php b/tests/jobs/email/ReviewReminderTest.php index 564a4e116dd..b4981530121 100644 --- a/tests/jobs/email/ReviewReminderTest.php +++ b/tests/jobs/email/ReviewReminderTest.php @@ -13,17 +13,18 @@ namespace PKP\tests\jobs\email; use Mockery; +use APP\core\Application; use PKP\tests\PKPTestCase; use PKP\jobs\email\ReviewReminder; use Illuminate\Support\Facades\Mail; use PKP\user\Repository as UserRepository; +use PHPUnit\Framework\Attributes\CoversClass; use PKP\log\event\Repository as EventRepository; use PKP\submission\reviewAssignment\ReviewAssignment; use APP\submission\Repository as SubmissionRepository; use PKP\emailTemplate\Repository as EmailTemplateRepository; -use PKP\submission\reviewAssignment\Repository as ReviewAssignmentRepository; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; -use PHPUnit\Framework\Attributes\CoversClass; +use PKP\submission\reviewAssignment\Repository as ReviewAssignmentRepository; #[RunTestsInSeparateProcesses] #[CoversClass(ReviewReminder::class)] @@ -36,6 +37,18 @@ class ReviewReminderTest extends PKPTestCase O:29:"PKP\\jobs\\email\\ReviewReminder":5:{s:9:"contextId";i:1;s:18:"reviewAssignmentId";i:57;s:13:"mailableClass";s:43:"PKP\mail\mailables\ReviewResponseRemindAuto";s:10:"connection";s:8:"database";s:5:"queue";s:5:"queue";} END; + /** + * @copydoc TestCase::setUp() + */ + protected function setUp(): void + { + parent::setUp(); + + if (Application::get()->getName() === 'ops') { + $this->markTestSkipped('OPS does not have reviewing system'); + } + } + /** * Test job is a proper instance */