first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
<?php
/**
* @file classes/article/ArticleTombstoneManager.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ArticleTombstoneManager
*
* @ingroup article
*
* @brief Class defining basic operations for article tombstones.
*/
namespace APP\article;
use APP\core\Application;
use APP\facades\Repo;
use APP\oai\ojs\OAIDAO;
use APP\submission\Submission;
use PKP\config\Config;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\plugins\Hook;
class ArticleTombstoneManager
{
/**
* Constructor
*/
public function __construct()
{
}
public function insertArticleTombstone($article, $journal, $section)
{
$tombstoneDao = DAORegistry::getDAO('DataObjectTombstoneDAO'); /** @var \PKP\tombstone\DataObjectTombstoneDAO $tombstoneDao */
// delete article tombstone -- to ensure that there aren't more than one tombstone for this article
$tombstoneDao->deleteByDataObjectId($article->getId());
// insert article tombstone
$setSpec = OAIDAO::setSpec($journal, $section);
$oaiIdentifier = 'oai:' . Config::getVar('oai', 'repository_id') . ':' . 'article/' . $article->getId();
$OAISetObjectsIds = [
Application::ASSOC_TYPE_JOURNAL => $journal->getId(),
Application::ASSOC_TYPE_SECTION => $section->getId(),
];
$articleTombstone = $tombstoneDao->newDataObject();
$articleTombstone->setDataObjectId($article->getId());
$articleTombstone->stampDateDeleted();
$articleTombstone->setSetSpec($setSpec);
$articleTombstone->setSetName($section->getLocalizedTitle());
$articleTombstone->setOAIIdentifier($oaiIdentifier);
$articleTombstone->setOAISetObjectsIds($OAISetObjectsIds);
$tombstoneDao->insertObject($articleTombstone);
if (Hook::call('ArticleTombstoneManager::insertArticleTombstone', [&$articleTombstone, &$article, &$journal, &$section])) {
return;
}
}
/**
* Insert tombstone for every published submission
*/
public function insertTombstonesByContext(Context $context)
{
$submissions = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getMany();
foreach ($submissions as $submission) {
$section = Repo::section()->get($submission->getSectionId());
$this->insertArticleTombstone($submission, $context, $section);
}
}
/**
* Delete tombstones for published submissions in this context
*/
public function deleteTombstonesByContextId(int $contextId)
{
$tombstoneDao = DAORegistry::getDAO('DataObjectTombstoneDAO'); /** @var \PKP\tombstone\DataObjectTombstoneDAO $tombstoneDao */
$submissions = Repo::submission()->getCollector()
->filterByContextIds([$contextId])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getMany();
foreach ($submissions as $submission) {
$tombstoneDao->deleteByDataObjectId($submission->getId());
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\article\ArticleTombstoneManager', '\ArticleTombstoneManager');
}
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* @file classes/author/Author.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Author
*
* @ingroup author
*
* @brief Article author metadata class.
*/
namespace APP\author;
class Author extends \PKP\author\Author
{
}
if (!PKP_STRICT_MODE) {
// Required for import/export toolset
class_alias('\APP\author\Author', '\Author');
}
+155
View File
@@ -0,0 +1,155 @@
<?php
/**
* @file classes/author/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @ingroup author
*
* @see Author
*
* @brief Operations for retrieving and modifying Author objects.
*/
namespace APP\author;
use APP\core\Application;
use APP\journal\Journal;
use APP\journal\JournalDAO;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
use PKP\facades\Locale;
use PKP\identity\Identity;
use PKP\submission\PKPSubmission;
class DAO extends \PKP\author\DAO
{
/**
* Retrieve all published authors for a journal by the first letter of the family name.
* Authors will be sorted by (family, given). Note that if journalId is null,
* alphabetized authors for all enabled journals are returned.
* If authors have the same given names, first names and affiliations in all journal locales,
* as well as country and email (optional), they are considered to be the same.
*
* @param int $journalId Optional journal ID to restrict results to
* @param string $initial An initial a family name must begin with, "-" for authors with no family names
* @param ?DBResultRange $rangeInfo Range information
* @param bool $includeEmail Whether or not to include the email in the select distinct
*
* @return DAOResultFactory<Author> Authors ordered by last name, given name
*
* @deprecated 3.4.0.0
*
*/
public function getAuthorsAlphabetizedByJournal($journalId = null, $initial = null, $rangeInfo = null, $includeEmail = false)
{
$locale = Locale::getLocale();
$params = [
Identity::IDENTITY_SETTING_GIVENNAME, $locale,
Identity::IDENTITY_SETTING_GIVENNAME,
Identity::IDENTITY_SETTING_FAMILYNAME, $locale,
Identity::IDENTITY_SETTING_FAMILYNAME,
'issueId',
];
if (isset($journalId)) {
$params[] = $journalId;
}
$supportedLocales = [];
if ($journalId !== null) {
/** @var JournalDAO */
$journalDao = DAORegistry::getDAO('JournalDAO');
/** @var Journal */
$journal = $journalDao->getById($journalId);
$supportedLocales = $journal->getSupportedLocales();
} else {
$site = Application::get()->getRequest()->getSite();
$supportedLocales = $site->getSupportedLocales();
}
$supportedLocalesCount = count($supportedLocales);
$sqlJoinAuthorSettings = $sqlColumnsAuthorSettings = $initialSql = '';
if (isset($initial)) {
$initialSql = ' AND (';
}
$index = 0;
foreach ($supportedLocales as $locale) {
$localeStr = str_replace('@', '_', $locale);
$sqlColumnsAuthorSettings .= ",
COALESCE(asg{$index}.setting_value, ''), ' ',
COALESCE(asf{$index}.setting_value, ''), ' ',
COALESCE(SUBSTRING(asa{$index}.setting_value FROM 1 FOR 255), ''), ' '
";
$sqlJoinAuthorSettings .= "
LEFT JOIN author_settings asg{$index} ON (asg{$index}.author_id = aa.author_id AND asg{$index}.setting_name = '" . Identity::IDENTITY_SETTING_GIVENNAME . "' AND asg{$index}.locale = '{$locale}')
LEFT JOIN author_settings asf{$index} ON (asf{$index}.author_id = aa.author_id AND asf{$index}.setting_name = '" . Identity::IDENTITY_SETTING_FAMILYNAME . "' AND asf{$index}.locale = '{$locale}')
LEFT JOIN author_settings asa{$index} ON (asa{$index}.author_id = aa.author_id AND asa{$index}.setting_name = 'affiliation' AND asa{$index}.locale = '{$locale}')
";
if (isset($initial)) {
if ($initial == '-') {
$initialSql .= "(asf{$index}.setting_value IS NULL OR asf{$index}.setting_value = '')";
if ($index < $supportedLocalesCount - 1) {
$initialSql .= ' AND ';
}
} else {
$params[] = PKPString::strtolower($initial) . '%';
$initialSql .= "LOWER(asf{$index}.setting_value) LIKE LOWER(?)";
if ($index < $supportedLocalesCount - 1) {
$initialSql .= ' OR ';
}
}
}
$index++;
}
if (isset($initial)) {
$initialSql .= ')';
}
$result = $this->deprecatedDao->retrieveRange(
$sql = 'SELECT a.*, ug.show_title, s.locale,
COALESCE(agl.setting_value, agpl.setting_value) AS author_given,
CASE WHEN agl.setting_value <> \'\' THEN afl.setting_value ELSE afpl.setting_value END AS author_family
FROM authors a
JOIN user_groups ug ON (a.user_group_id = ug.user_group_id)
JOIN publications p ON (p.publication_id = a.publication_id)
JOIN submissions s ON (s.current_publication_id = p.publication_id)
LEFT JOIN author_settings agl ON (a.author_id = agl.author_id AND agl.setting_name = ? AND agl.locale = ?)
LEFT JOIN author_settings agpl ON (a.author_id = agpl.author_id AND agpl.setting_name = ? AND agpl.locale = s.locale)
LEFT JOIN author_settings afl ON (a.author_id = afl.author_id AND afl.setting_name = ? AND afl.locale = ?)
LEFT JOIN author_settings afpl ON (a.author_id = afpl.author_id AND afpl.setting_name = ? AND afpl.locale = s.locale)
JOIN (
SELECT
MIN(aa.author_id) as author_id,
CONCAT(
' . ($includeEmail ? 'aa.email, \' \', ' : '') . '
ac.setting_value,
\' \'
' . $sqlColumnsAuthorSettings . '
) as names
FROM authors aa
JOIN publications pp ON (pp.publication_id = aa.publication_id)
LEFT JOIN publication_settings ppss ON (ppss.publication_id = pp.publication_id)
JOIN submissions ss ON (ss.submission_id = pp.submission_id AND ss.current_publication_id = pp.publication_id AND ss.status = ' . PKPSubmission::STATUS_PUBLISHED . ')
JOIN journals j ON (ss.context_id = j.journal_id)
JOIN issues i ON (ppss.setting_name = ? AND ppss.setting_value = CAST(i.issue_id AS CHAR(20)) AND i.published = 1)
LEFT JOIN author_settings ac ON (ac.author_id = aa.author_id AND ac.setting_name = \'country\')
' . $sqlJoinAuthorSettings . '
WHERE j.enabled = 1
' . (isset($journalId) ? ' AND j.journal_id = ?' : '')
. $initialSql . '
GROUP BY names
) as t1 ON (t1.author_id = a.author_id)
ORDER BY author_family, author_given',
$params,
$rangeInfo
);
return new DAOResultFactory($result, $this, 'fromRow', [], $sql, $params, $rangeInfo);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* @file components/StatsIssuePage.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StatsIssuePage
*
* @ingroup classes_controllers_stats
*
* @brief A class to prepare the data object for the issue statistics
* UI component
*/
namespace APP\components;
use PKP\components\PKPStatsComponent;
use PKP\statistics\PKPStatisticsHelper;
class StatsIssuePage extends PKPStatsComponent
{
/** @var array A timeline of stats (eg - monthly) for a graph */
public $timeline = [];
/** @var string Which time segment (eg - month) is displayed in the graph */
public $timelineInterval = PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH;
/** @var string Which views to show in the graph. Supports `abstract` or `galley`. */
public $timelineType = '';
/** @var array List of items to display stats for */
public $items = [];
/** @var int The maximum number of items that stats can be shown for */
public $itemsMax = 0;
/** @var int How many items to show per page */
public $count = 30;
/** @var string Order items by this property */
public $orderBy = '';
/** @var string Order items in this direction: ASC or DESC*/
public $orderDirection = 'DESC';
/** @var string A search phrase to filter the list of items */
public $searchPhrase = null;
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
$config = parent::getConfig();
$config = array_merge(
$config,
[
'timeline' => $this->timeline,
'timelineInterval' => $this->timelineInterval,
'timelineType' => $this->timelineType,
'items' => $this->items,
'dateRangeLabel' => __('stats.dateRange'),
'searchPhraseLabel' => __('common.searchPhrase'),
'itemsOfTotalLabel' => __('stats.issues.countOfTotal'),
'betweenDatesLabel' => __('stats.downloadReport.betweenDates'),
'allDatesLabel' => __('stats.dateRange.allDates'),
'timelineTypeLabel' => __('stats.timelineType'),
'timelineIntervalLabel' => __('stats.timelineInterval'),
'viewsLabel' => __('submission.views'),
'downloadsLabel' => __('submission.downloads'),
'dayLabel' => __('common.day'),
'monthLabel' => __('common.month'),
'timelineDescriptionLabel' => __('stats.timeline.downloadReport.description'),
'itemsMax' => $this->itemsMax,
'count' => $this->count,
'offset' => 0,
'searchPhrase' => null,
'orderBy' => $this->orderBy,
'orderDirection' => $this->orderDirection,
'isLoadingTimeline' => false,
]
);
return $config;
}
}
@@ -0,0 +1,60 @@
<?php
/**
* @file classes/components/form/FieldArchivingPn.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldArchivingPn
*
* @ingroup classes_controllers_form
*
* @brief An extension of the FieldOptions for the configuration setting which
* allows a user to enable/disable the PKP Preservation Network plugin, and
* access the settings, from a form-like view in the distribution settings.
*/
namespace APP\components\forms;
use PKP\components\forms\FieldOptions;
class FieldArchivingPn extends FieldOptions
{
/** @copydoc Field::$component */
public $component = 'field-archiving-pn';
/** @var string The message to show in a modal when the link is clicked. */
public $terms = '';
/** @var string The message to show when the plugin is disabled. */
public $disablePluginSuccess = '';
/** @var string The message to show when the plugin was enabled.. */
public $enablePluginSuccess = '';
/** @var string The URL to enable the PLN plugin. */
public $enablePluginUrl = '';
/** @var string The URL to disable the PLN plugin. */
public $disablePluginUrl = '';
/** @var string The URL to load the PN plugin settings. */
public $settingsUrl = '';
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['terms'] = $this->terms;
$config['disablePluginSuccess'] = $this->disablePluginSuccess;
$config['enablePluginSuccess'] = $this->enablePluginSuccess;
$config['enablePluginUrl'] = $this->enablePluginUrl;
$config['disablePluginUrl'] = $this->disablePluginUrl;
$config['settingsUrl'] = $this->settingsUrl;
return $config;
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @file classes/components/form/FieldSelectIssue.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldSelectIssue
*
* @ingroup classes_controllers_form
*
* @brief An extension of the FieldSelect for selecting an issue.
*/
namespace APP\components\forms;
use APP\core\Application;
use PKP\components\forms\FieldSelect;
class FieldSelectIssue extends FieldSelect
{
/** @copydoc Field::$component */
public $component = 'field-select-issue';
/** @var int One of the PKPSubmission::STATUS_ constants */
public $publicationStatus;
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['publicationStatus'] = $this->publicationStatus;
$issueUrlPlaceholder = Application::get()->getRequest()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
null,
'issue',
'view',
'__issueId__'
);
$config['assignLabel'] = __('publication.assignToissue');
$config['assignedNoticeBase'] = __('publication.assignedToIssue', ['issueUrl' => $issueUrlPlaceholder]);
$config['changeIssueLabel'] = __('publication.changeIssue');
$config['publishedNoticeBase'] = __('publication.publishedIn', ['issueUrl' => $issueUrlPlaceholder]);
$config['scheduledNoticeBase'] = __('publication.scheduledIn', ['issueUrl' => $issueUrlPlaceholder]);
$config['unscheduledNotice'] = __('publication.unscheduledIn');
$config['unscheduleLabel'] = __('publication.unschedule');
return $config;
}
}
@@ -0,0 +1,24 @@
<?php
/**
* @file classes/components/form/FieldSelectIssues.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldSelectIssues
*
* @ingroup classes_controllers_form
*
* @brief A text field to search for and select issues.
*/
namespace APP\components\forms;
use PKP\components\forms\FieldBaseAutosuggest;
class FieldSelectIssues extends FieldBaseAutosuggest
{
/** @copydoc Field::$component */
public $component = 'field-select-issues';
}
@@ -0,0 +1,83 @@
<?php
/**
* @file classes/components/form/context/AccessForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AccessForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for configuring the terms under which a journal will
* allow access to its published content.
*/
namespace APP\components\forms\context;
use APP\journal\Journal;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldSelect;
use PKP\components\forms\FormComponent;
define('FORM_ACCESS', 'access');
define('SUBSCRIPTION_OPEN_ACCESS_DELAY_MIN', '1');
define('SUBSCRIPTION_OPEN_ACCESS_DELAY_MAX', '60');
class AccessForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ACCESS;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param Journal $context Journal to change settings for
*/
public function __construct($action, $locales, $context)
{
$this->action = $action;
$this->locales = $locales;
$validDelayedOpenAccessDuration[] = ['value' => 0, 'label' => __('common.disabled')];
for ($i = SUBSCRIPTION_OPEN_ACCESS_DELAY_MIN; $i <= SUBSCRIPTION_OPEN_ACCESS_DELAY_MAX; $i++) {
$validDelayedOpenAccessDuration[] = [
'value' => $i,
'label' => __('manager.subscriptionPolicies.xMonths', ['x' => $i]),
];
}
$this->addField(new FieldOptions('publishingMode', [
'label' => __('manager.distribution.publishingMode'),
'type' => 'radio',
'options' => [
['value' => Journal::PUBLISHING_MODE_OPEN, 'label' => __('manager.distribution.publishingMode.openAccess')],
['value' => Journal::PUBLISHING_MODE_SUBSCRIPTION, 'label' => __('manager.distribution.publishingMode.subscription')],
['value' => Journal::PUBLISHING_MODE_NONE, 'label' => __('manager.distribution.publishingMode.none')],
],
'value' => $context->getData('publishingMode'),
]))
->addField(new FieldSelect('delayedOpenAccessDuration', [
'label' => __('about.delayedOpenAccess'),
'options' => $validDelayedOpenAccessDuration,
'value' => $context->getData('delayedOpenAccessDuration'),
'showWhen' => ['publishingMode', Journal::PUBLISHING_MODE_SUBSCRIPTION],
]))
->addField(new FieldOptions('enableOai', [
'label' => __('manager.setup.enableOai'),
'description' => __('manager.setup.enableOai.description'),
'type' => 'radio',
'options' => [
['value' => true, 'label' => __('common.enable')],
['value' => false, 'label' => __('common.disable')],
],
'value' => $context->getData('enableOai'),
]));
}
}
@@ -0,0 +1,23 @@
<?php
/**
* @file classes/components/form/context/AppearanceAdvancedForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AppearanceAdvancedForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for general website appearance setup, such as uploading
* a logo.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPAppearanceAdvancedForm;
class AppearanceAdvancedForm extends PKPAppearanceAdvancedForm
{
}
@@ -0,0 +1,42 @@
<?php
/**
* @file classes/components/form/context/AppearanceSetupForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AppearanceSetupForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for general website appearance setup, such as uploading
* a logo.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPAppearanceSetupForm;
use PKP\components\forms\FieldUploadImage;
class AppearanceSetupForm extends PKPAppearanceSetupForm
{
/**
* @copydoc PKPAppearanceSetupForm::__construct()
*/
public function __construct($action, $locales, $context, $baseUrl, $temporaryFileApiUrl, $uploadImageUrl)
{
parent::__construct($action, $locales, $context, $baseUrl, $temporaryFileApiUrl, $uploadImageUrl);
$this->addField(new FieldUploadImage('journalThumbnail', [
'label' => __('manager.setup.journalThumbnail'),
'tooltip' => __('manager.setup.journalThumbnail.description'),
'isMultilingual' => true,
'value' => $context->getData('journalThumbnail'),
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
],
]), [FIELD_POSITION_AFTER, 'pageHeaderLogoImage']);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @file classes/components/form/context/ArchivingLockssForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ArchivingLockssForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for configuring the LOCKSS and CLOCKSS settings.
*/
namespace APP\components\forms\context;
use APP\journal\Journal;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FormComponent;
define('FORM_ARCHIVING_LOCKSS', 'archivingLockss');
class ArchivingLockssForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ARCHIVING_LOCKSS;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param Journal $context Journal or Press to change settings for
* @param string $lockssUrl URL to the publisher manifest page for LOCKSS
* @param string $clockssUrl URL to the publisher manifest page for CLOCKSS
*/
public function __construct($action, $locales, $context, $lockssUrl, $clockssUrl)
{
$this->action = $action;
$this->locales = $locales;
$this->addField(new FieldOptions('enableLockss', [
'label' => __('manager.setup.lockssTitle'),
'description' => __('manager.setup.lockssLicenseDescription'),
'options' => [
[
'value' => true,
'label' => __('manager.setup.lockssEnable', ['lockssUrl' => $lockssUrl]),
],
],
'value' => (bool) $context->getData('enableLockss'),
]))
->addField(new FieldOptions('enableClockss', [
'label' => __('manager.setup.clockssTitle'),
'description' => __('manager.setup.clockssLicenseDescription'),
'options' => [
[
'value' => true,
'label' => __('manager.setup.clockssEnable', ['clockssUrl' => $clockssUrl]),
],
],
'value' => (bool) $context->getData('enableClockss'),
]));
}
}
@@ -0,0 +1,44 @@
<?php
/**
* @file classes/components/form/context/ContextForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ContextForm
*
* @ingroup classes_controllers_form
*
* @brief Add OJS-specific fields to the context add/edit form.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPContextForm;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldText;
class ContextForm extends PKPContextForm
{
/**
* @copydoc PKPContextForm::__construct()
*/
public function __construct($action, $locales, $baseUrl, $context)
{
parent::__construct($action, $locales, $baseUrl, $context);
$this->addField(new FieldText('abbreviation', [
'label' => __('manager.setup.journalAbbreviation'),
'isMultilingual' => true,
'value' => $context ? $context->getData('abbreviation') : null,
]), [FIELD_POSITION_AFTER, 'acronym'])
->addField(new FieldOptions('enabled', [
'label' => __('common.enable'),
'options' => [
['value' => true, 'label' => __('admin.journals.enableJournalInstructions')],
],
'value' => $context ? (bool) $context->getData('enabled') : false,
]));
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @file classes/components/form/context/DoiSetupSettingsForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DoiSetupSettingsForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for enabling and configuring DOI settings for a given context
*/
namespace APP\components\forms\context;
use APP\facades\Repo;
use PKP\components\forms\context\PKPDoiSetupSettingsForm;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldText;
use PKP\context\Context;
use PKP\plugins\Hook;
class DoiSetupSettingsForm extends PKPDoiSetupSettingsForm
{
public function __construct(string $action, array $locales, Context $context)
{
parent::__construct($action, $locales, $context);
$this->objectTypeOptions = [
[
'value' => Repo::doi()::TYPE_PUBLICATION,
'label' => __('article.articles'),
'allowedBy' => [],
],
[
'value' => Repo::doi()::TYPE_ISSUE,
'label' => __('issue.issues'),
'allowedBy' => [],
],
[
'value' => Repo::doi()::TYPE_REPRESENTATION,
'label' => __('doi.manager.settings.galleysWithDescription'),
'allowedBy' => [],
]
];
Hook::call('DoiSetupSettingsForm::getObjectTypes', [&$this->objectTypeOptions]);
if ($this->enabledRegistrationAgency === null) {
$filteredOptions = $this->objectTypeOptions;
} else {
$filteredOptions = array_filter($this->objectTypeOptions, function ($option) {
return in_array($this->enabledRegistrationAgency, $option['allowedBy']);
});
}
$this->addField(new FieldOptions(Context::SETTING_ENABLED_DOI_TYPES, [
'label' => __('doi.manager.settings.doiObjects'),
'description' => __('doi.manager.settings.doiObjectsRequired'),
'groupId' => self::DOI_SETTINGS_GROUP,
'options' => $filteredOptions,
'value' => $context->getData(Context::SETTING_ENABLED_DOI_TYPES) ? $context->getData(Context::SETTING_ENABLED_DOI_TYPES) : [],
]), [FIELD_POSITION_BEFORE, Context::SETTING_DOI_PREFIX])
->addField(new FieldText(Repo::doi()::CUSTOM_ISSUE_PATTERN, [
'label' => __('issue.issues'),
'groupId' => self::DOI_CUSTOM_SUFFIX_GROUP,
'value' => $context->getData(Repo::doi()::CUSTOM_ISSUE_PATTERN),
]));
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @file classes/components/form/context/LicenseForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class LicenseForm
*
* @ingroup classes_controllers_form
*
* @brief Add OJS-specific details to the license settings forms
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPLicenseForm;
use PKP\components\forms\FieldOptions;
class LicenseForm extends PKPLicenseForm
{
/** @copydoc FormComponent::$id */
public $id = FORM_LICENSE;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* @copydoc PKPLicenseForm::__construct()
*/
public function __construct($action, $locales, $context)
{
parent::__construct($action, $locales, $context);
$this->addField(new FieldOptions('copyrightYearBasis', [
'label' => __('submission.copyrightYear'),
'description' => __('manager.distribution.copyrightYearBasis.description'),
'type' => 'radio',
'options' => [
['value' => 'issue', 'label' => __('manager.distribution.copyrightYearBasis.issue')],
['value' => 'submission', 'label' => __('manager.distribution.copyrightYearBasis.submission')],
],
'value' => $context->getData('copyrightYearBasis'),
]), [FIELD_POSITION_AFTER, 'licenseUrl']);
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @file classes/components/form/context/MastheadForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MastheadForm
*
* @ingroup classes_controllers_form
*
* @brief Add OJS-specific fields to the masthead form.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPMastheadForm;
use PKP\components\forms\FieldText;
class MastheadForm extends PKPMastheadForm
{
/**
* @copydoc PKPMastheadForm::__construct()
*/
public function __construct($action, $locales, $context, $imageUploadUrl)
{
parent::__construct($action, $locales, $context, $imageUploadUrl);
$this->addField(new FieldText('abbreviation', [
'label' => __('manager.setup.journalAbbreviation'),
'isMultilingual' => true,
'groupId' => 'identity',
'value' => $context->getData('abbreviation'),
]))
->addField(new FieldText('publisherInstitution', [
'label' => __('manager.setup.publisher'),
'groupId' => 'publishing',
'value' => $context->getData('publisherInstitution'),
]))
->addField(new FieldText('publisherUrl', [
'label' => __('common.url'),
'groupId' => 'publishing',
'value' => $context->getData('publisherUrl'),
]))
->addField(new FieldText('onlineIssn', [
'label' => __('manager.setup.onlineIssn'),
'size' => 'small',
'groupId' => 'publishing',
'value' => $context->getData('onlineIssn'),
]))
->addField(new FieldText('printIssn', [
'label' => __('manager.setup.printIssn'),
'size' => 'small',
'groupId' => 'publishing',
'value' => $context->getData('printIssn'),
]));
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @file classes/components/form/context/MetadataSettingsForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MetadataSettingsForm
*
* @ingroup classes_controllers_form
*
* @brief Add OJS-specific fields to the masthead form.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPMetadataSettingsForm;
use PKP\components\forms\FieldOptions;
class MetadataSettingsForm extends PKPMetadataSettingsForm
{
/**
* @copydoc PKPMetadataSettingsForm::__construct()
*/
public function __construct($action, $context)
{
parent::__construct($action, $context);
$this->addField(new FieldOptions('enablePublisherId', [
'label' => __('submission.publisherId'),
'description' => __('submission.publisherId.description'),
'options' => [
[
'value' => 'publication',
'label' => __('submission.publisherId.enable', ['objects' => __('submission.publications')]),
],
[
'value' => 'galley',
'label' => __('submission.publisherId.enable', ['objects' => __('submission.layout.galleys')]),
],
[
'value' => 'issue',
'label' => __('submission.publisherId.enable', ['objects' => __('issue.issues')]),
],
[
'value' => 'issueGalley',
'label' => __('submission.publisherId.enable', ['objects' => __('editor.issues.galleys')]),
],
],
'value' => $context->getData('enablePublisherId') ? $context->getData('enablePublisherId') : [],
]));
}
}
@@ -0,0 +1,22 @@
<?php
/**
* @file classes/components/form/context/ReviewGuidanceForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewGuidanceForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for configuring the guidance a reviewer should receive.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPReviewGuidanceForm;
class ReviewGuidanceForm extends PKPReviewGuidanceForm
{
}
@@ -0,0 +1,38 @@
<?php
/**
* @file classes/components/form/context/UserAccessForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class UserAccessForm
*
* @ingroup classes_controllers_form
*
* @brief Add OJS-specific fields to the users and roles access settings form.
*/
namespace APP\components\forms\context;
use PKP\components\forms\context\PKPUserAccessForm;
use PKP\components\forms\FieldOptions;
class UserAccessForm extends PKPUserAccessForm
{
/**
* @copydoc PKPUserAccessForm::__construct()
*/
public function __construct($action, $context)
{
parent::__construct($action, $context);
$this->addField(new FieldOptions('restrictArticleAccess', [
'label' => __('manager.setup.siteAccess.viewContent'),
'value' => (bool) $context->getData('restrictArticleAccess'),
'options' => [
['value' => true, 'label' => __('manager.setup.restrictArticleAccess')],
],
]), [FIELD_POSITION_AFTER, 'restrictSiteAccess']);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @file classes/components/form/counter/CounterReportForm.php
*
* 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.
*
* @class CounterReportForm
*
* @ingroup classes_controllers_form
*
* @brief A form for setting a counter report
*/
namespace APP\components\forms\counter;
use APP\sushi\IR;
use APP\sushi\IR_A1;
use APP\sushi\PR;
use APP\sushi\PR_P1;
use APP\sushi\TR;
use APP\sushi\TR_J3;
use PKP\components\forms\counter\PKPCounterReportForm;
class CounterReportForm extends PKPCounterReportForm
{
public function setReportFields(): void
{
$formFieldsPR = PR::getReportSettingsFormFields();
$this->reportFields['PR'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsPR);
$formFieldsPR_P1 = PR_P1::getReportSettingsFormFields();
$this->reportFields['PR_P1'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsPR_P1);
$formFieldsTR = TR::getReportSettingsFormFields();
$this->reportFields['TR'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsTR);
$formFieldsTR_J3 = TR_J3::getReportSettingsFormFields();
$this->reportFields['TR_J3'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsTR_J3);
$formFieldsIR = IR::getReportSettingsFormFields();
$this->reportFields['IR'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsIR);
$formFieldsIR_A1 = IR_A1::getReportSettingsFormFields();
$this->reportFields['IR_A1'] = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $formFieldsIR_A1);
}
}
@@ -0,0 +1,57 @@
<?php
/**
* @file classes/components/form/decision/RequestPaymentDecisionForm.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RequestPaymentDecisionForm
*
* @ingroup classes_controllers_form
*
* @brief A form to request or waive an APC payment when making an editorial decision
*/
namespace APP\components\forms\decision;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FormComponent;
use PKP\context\Context;
define('FORM_REQUEST_PAYMENT_DECISION', 'requestPaymentDecision');
class RequestPaymentDecisionForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_REQUEST_PAYMENT_DECISION;
/** @copydoc FormComponent::$action */
public $action = FormComponent::ACTION_EMIT;
/**
* Constructor
*/
public function __construct(Context $context)
{
$this->addField(new FieldOptions('requestPayment', [
'label' => __('common.payment'),
'type' => 'radio',
'options' => [
[
'value' => true,
'label' => __(
'payment.requestPublicationFee',
['feeAmount' => $context->getData('publicationFee') . ' ' . $context->getData('currency')]
),
],
[
'value' => false,
'label' => __('payment.waive'),
],
],
'value' => true,
'groupId' => 'default',
]));
}
}
@@ -0,0 +1,82 @@
<?php
/**
* @file classes/components/form/publication/AssignToIssueForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AssignToIssueForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for setting a publication's issue.
*/
namespace APP\components\forms\publication;
use APP\facades\Repo;
use PKP\components\forms\FieldSelect;
use PKP\components\forms\FormComponent;
define('FORM_ASSIGN_TO_ISSUE', 'assignToIssue');
class AssignToIssueForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ASSIGN_TO_ISSUE;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param \APP\publication\Publication $publication The publication to change settings for
* @param \APP\journal\Journal $publicationContext The context of the publication
*/
public function __construct($action, $publication, $publicationContext)
{
$this->action = $action;
// Issue options
$issueOptions = [['value' => '', 'label' => '']];
$unpublishedIssues = Repo::issue()->getCollector()
->filterByContextIds([$publicationContext->getId()])
->filterByPublished(false)
->getMany();
if ($unpublishedIssues->count() > 0) {
$issueOptions[] = ['value' => '', 'label' => '--- ' . __('editor.issues.futureIssues') . ' ---'];
foreach ($unpublishedIssues as $issue) {
$issueOptions[] = [
'value' => (int) $issue->getId(),
'label' => htmlspecialchars($issue->getIssueIdentification()),
];
}
}
$publishedIssues = Repo::issue()->getCollector()
->filterByContextIds([$publicationContext->getId()])
->filterByPublished(true)
->getMany();
if ($publishedIssues->count() > 0) {
$issueOptions[] = ['value' => '', 'label' => '--- ' . __('editor.issues.backIssues') . ' ---'];
foreach ($publishedIssues as $issue) {
$issueOptions[] = [
'value' => (int) $issue->getId(),
'label' => htmlspecialchars($issue->getIssueIdentification()),
];
}
}
$this->addField(new FieldSelect('issueId', [
'label' => __('issue.issue'),
'options' => $issueOptions,
'value' => $publication->getData('issueId') ? $publication->getData('issueId') : 0,
]));
}
}
@@ -0,0 +1,158 @@
<?php
/**
* @file classes/components/form/publication/IssueEntryForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueEntryForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for setting a publication's issue, section, categories,
* pages, etc.
*/
namespace APP\components\forms\publication;
use APP\components\forms\FieldSelectIssue;
use APP\facades\Repo;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldSelect;
use PKP\components\forms\FieldText;
use PKP\components\forms\FieldUploadImage;
use PKP\components\forms\FormComponent;
define('FORM_ISSUE_ENTRY', 'issueEntry');
class IssueEntryForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ISSUE_ENTRY;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param \APP\publication\Publication $publication The publication to change settings for
* @param \APP\journal\Journal $publicationContext The context of the publication
* @param string $baseUrl Site's base URL. Used for image previews.
* @param string $temporaryFileApiUrl URL to upload files to
*/
public function __construct($action, $locales, $publication, $publicationContext, $baseUrl, $temporaryFileApiUrl)
{
$this->action = $action;
$this->locales = $locales;
// Issue options
$issueOptions = [['value' => '', 'label' => '']];
$unpublishedIssues = Repo::issue()->getCollector()
->filterByContextIds([$publicationContext->getId()])
->filterByPublished(false)
->getMany()
->toArray();
if (count($unpublishedIssues)) {
$issueOptions[] = ['value' => '', 'label' => '--- ' . __('editor.issues.futureIssues') . ' ---'];
foreach ($unpublishedIssues as $issue) {
$issueOptions[] = [
'value' => (int) $issue->getId(),
'label' => htmlspecialchars($issue->getIssueIdentification()),
];
}
}
$publishedIssues = Repo::issue()->getCollector()
->filterByContextIds([$publicationContext->getId()])
->filterByPublished(true)
->getMany()
->toArray();
if (count($publishedIssues)) {
$issueOptions[] = ['value' => '', 'label' => '--- ' . __('editor.issues.backIssues') . ' ---'];
foreach ($publishedIssues as $issue) {
$issueOptions[] = [
'value' => (int) $issue->getId(),
'label' => htmlspecialchars($issue->getIssueIdentification()),
];
}
}
// Section options
$sections = Repo::section()->getSectionList($publicationContext->getId());
$sectionOptions = [];
foreach ($sections as $section) {
$sectionOptions[] = [
'label' => (($section['group']) ? __('publication.inactiveSection', ['section' => $section['title']]) : $section['title']),
'value' => (int) $section['id'],
];
}
$this->addField(new FieldSelectIssue('issueId', [
'label' => __('issue.issue'),
'options' => $issueOptions,
'publicationStatus' => $publication->getData('status'),
'value' => $publication->getData('issueId') ? $publication->getData('issueId') : 0,
]))
->addField(new FieldSelect('sectionId', [
'label' => __('section.section'),
'options' => $sectionOptions,
'value' => (int) $publication->getData('sectionId'),
]));
// Categories
$categoryOptions = [];
$categories = Repo::category()->getCollector()
->filterByContextIds([$publicationContext->getId()])
->getMany()
->toArray();
foreach ($categories as $category) {
$label = $category->getLocalizedTitle();
if ($category->getParentId()) {
$label = $categories[$category->getParentId()]->getLocalizedTitle() . ' > ' . $label;
}
$categoryOptions[] = [
'value' => (int) $category->getId(),
'label' => $label,
];
}
if (!empty($categoryOptions)) {
$this->addField(new FieldOptions('categoryIds', [
'label' => __('submission.submit.placement.categories'),
'value' => $publication->getData('categoryIds'),
'options' => $categoryOptions,
]));
}
$this->addField(new FieldUploadImage('coverImage', [
'label' => __('editor.article.coverImage'),
'value' => $publication->getData('coverImage'),
'isMultilingual' => true,
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
],
]))
->addField(new FieldText('pages', [
'label' => __('editor.issues.pages'),
'value' => $publication->getData('pages'),
]))
->addField(new FieldText('urlPath', [
'label' => __('publication.urlPath'),
'description' => __('publication.urlPath.description'),
'value' => $publication->getData('urlPath'),
]))
->addField(new FieldText('datePublished', [
'label' => __('publication.datePublished'),
'description' => __('publication.datePublished.description'),
'value' => $publication->getData('datePublished'),
'size' => 'small',
]));
}
}
@@ -0,0 +1,112 @@
<?php
/**
* @file classes/components/form/publication/PublishForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PublishForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for confirming a publication's issue before publishing.
* It may also be used for scheduling a publication in an issue for later
* publication.
*/
namespace APP\components\forms\publication;
use APP\facades\Repo;
use PKP\components\forms\FieldHTML;
use PKP\components\forms\FormComponent;
use PKP\core\Core;
use PKP\core\PKPString;
define('FORM_PUBLISH', 'publish');
class PublishForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_PUBLISH;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/** @var \APP\publication\Publication */
public $publication;
/** @var \APP\journal\Journal */
public $submissionContext;
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param \APP\publication\Publication $publication The publication to change settings for
* @param \APP\journal\Journal $submissionContext journal or press
* @param array $requirementErrors A list of pre-publication requirements that are not met.
*/
public function __construct($action, $publication, $submissionContext, $requirementErrors)
{
$this->action = $action;
$this->errors = $requirementErrors;
$this->publication = $publication;
$this->submissionContext = $submissionContext;
// Set separate messages and buttons if publication requirements have passed
if (empty($requirementErrors)) {
$msg = __('publication.publish.confirmation');
$submitLabel = __('publication.publish');
if ($publication->getData('issueId')) {
$issue = Repo::issue()->get($publication->getData('issueId'));
if ($issue) {
if ($issue->getData('published')) {
$msg = __('publication.publish.confirmation.backIssue', ['issue' => htmlspecialchars($issue->getIssueIdentification())]);
} else {
$msg = __('publication.publish.confirmation.futureIssue', ['issue' => htmlspecialchars($issue->getIssueIdentification())]);
$submitLabel = __('editor.submission.schedulePublication');
}
}
}
// If a publication date has already been set and the date has passed this will
// be published immediately regardless of the issue assignment
if ($publication->getData('datePublished') && $publication->getData('datePublished') <= Core::getCurrentDate()) {
$timestamp = strtotime($publication->getData('datePublished'));
$dateFormatLong = PKPString::convertStrftimeFormat($submissionContext->getLocalizedDateFormatLong());
$msg = __(
'publication.publish.confirmation.datePublishedInPast',
[
'datePublished' => date($dateFormatLong, $timestamp),
]
);
$submitLabel = __('publication.publish');
}
$this->addPage([
'id' => 'default',
'submitButton' => [
'label' => $submitLabel,
],
]);
} else {
$msg = '<p>' . __('publication.publish.requirements') . '</p>';
$msg .= '<ul>';
foreach ($requirementErrors as $error) {
$msg .= '<li>' . $error . '</li>';
}
$msg .= '</ul>';
$this->addPage([
'id' => 'default',
]);
}
$this->addGroup([
'id' => 'default',
'pageId' => 'default',
])
->addField(new FieldHTML('validation', [
'description' => $msg,
'groupId' => 'default',
]));
}
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/components/form/publication/SubmissionPaymentsForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionPaymentsForm
*
* @ingroup classes_controllers_form
*
* @brief A form for managing submission fees.
*/
namespace APP\components\forms\publication;
use APP\payment\ojs\OJSCompletedPaymentDAO;
use APP\payment\ojs\OJSPaymentManager;
use PKP\components\forms\FieldRadioInput;
use PKP\components\forms\FormComponent;
use PKP\db\DAORegistry;
define('FORM_SUBMISSION_PAYMENTS', 'submissionPayments');
class SubmissionPaymentsForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_SUBMISSION_PAYMENTS;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param \APP\submission\Submission $submission The submission to inspect payment status of
* @param \APP\journal\Journal $submissionContext The context of the submission
*/
public function __construct($action, $submission, $submissionContext)
{
$this->action = $action;
$completedPaymentDao = DAORegistry::getDAO('OJSCompletedPaymentDAO'); /** @var OJSCompletedPaymentDAO $completedPaymentDao */
$publicationFeePayment = $completedPaymentDao->getByAssoc(null, OJSPaymentManager::PAYMENT_TYPE_PUBLICATION, $submission->getId());
$this->addField(new FieldRadioInput('publicationFeeStatus', [
'label' => __('payment.type.publication'),
'type' => 'radio',
'options' => [
['value' => 'waived', 'label' => __('payment.waived')],
['value' => 'paid', 'label' => __('payment.paid')],
['value' => 'unpaid', 'label' => __('payment.unpaid')],
],
'value' => $publicationFeePayment
? ($publicationFeePayment->getAmount() ? 'paid' : 'waived')
: 'unpaid'
]));
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @file classes/components/form/submission/ReconfigureSubmission.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReconfigureSubmission
*
* @ingroup classes_controllers_form
*
* @brief A preset form for configuring the submission wizard, such as the
* submission's section or language, after the submission was started.
*/
namespace APP\components\forms\submission;
use APP\publication\Publication;
use APP\section\Section;
use APP\submission\Submission;
use PKP\components\forms\FieldHTML;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\submission\ReconfigureSubmission as BaseReconfigureSubmission;
use PKP\context\Context;
use PKP\core\PKPString;
class ReconfigureSubmission extends BaseReconfigureSubmission
{
public array $sections;
/**
* @param Section[] $sections
*/
public function __construct(string $action, Submission $submission, Publication $publication, Context $context, array $sections)
{
parent::__construct($action, $submission, $publication, $context);
$this->sections = $sections;
if (count($this->sections) > 1) {
$this->addSectionsField();
}
}
protected function addSectionsField(): void
{
$this->addField(new FieldOptions('sectionId', [
'type' => 'radio',
'label' => __('section.section'),
'description' => __('author.submit.journalSectionDescription'),
'options' => $this->getSectionOptions(),
'isRequired' => true,
'value' => $this->publication->getData('sectionId'),
]));
foreach ($this->sections as $section) {
if (!trim(PKPString::html2text($section->getLocalizedPolicy()))) {
continue;
}
$this->addField(new FieldHTML('sectionDescription' . $section->getId(), [
'label' => $section->getLocalizedTitle(),
'description' => $section->getLocalizedPolicy(),
'showWhen' => ['sectionId', $section->getId()],
]));
}
}
/**
* Convert sections to options prop for a FieldOption
*/
protected function getSectionOptions(): array
{
$options = [];
foreach ($this->sections as $section) {
$options[] = [
'value' => $section->getId(),
'label' => $section->getLocalizedTitle(),
];
}
return $options;
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/components/form/submission/StartSubmission.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StartSubmission
*
* @ingroup classes_controllers_form
*
* @brief The form to begin the submission wizard
*/
namespace APP\components\forms\submission;
use APP\section\Section;
use Illuminate\Support\Enumerable;
use PKP\components\forms\FieldHTML;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\submission\StartSubmission as BaseStartSubmission;
use PKP\context\Context;
use PKP\core\PKPString;
class StartSubmission extends BaseStartSubmission
{
/**
* @param Section[] $sections The sections that this user can submit to
*/
public function __construct(string $action, Context $context, Enumerable $userGroups, array $sections)
{
parent::__construct($action, $context, $userGroups);
if (count($sections) === 1) {
$this->addHiddenField('sectionId', $sections[0]->getId());
} else {
$this->addField(new FieldOptions('sectionId', [
'type' => 'radio',
'label' => __('section.section'),
'description' => __('author.submit.journalSectionDescription'),
'options' => $this->getSectionOptions($sections),
'value' => '',
'isRequired' => true,
]), [FIELD_POSITION_AFTER, 'title']);
foreach ($sections as $section) {
if (!trim(PKPString::html2text($section->getLocalizedPolicy()))) {
continue;
}
$this->addField(new FieldHTML('sectionDescription' . $section->getId(), [
'label' => $section->getLocalizedTitle(),
'description' => $section->getLocalizedPolicy(),
'showWhen' => ['sectionId', $section->getId()],
]), [FIELD_POSITION_AFTER, 'sectionId']);
}
}
}
/**
* Convert sections to options prop for a FieldOption
*
* @param Section[] $sections
*/
protected function getSectionOptions(array $sections): array
{
$options = [];
foreach ($sections as $section) {
$options[] = [
'value' => $section->getId(),
'label' => $section->getLocalizedTitle(),
];
}
return $options;
}
}
@@ -0,0 +1,79 @@
<?php
/**
* @file classes/components/listPanels/DoiListPanel.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DoiListPanel
*
* @ingroup classes_components_list
*
* @brief A ListPanel component for viewing and editing DOIs
*/
namespace APP\components\listPanels;
use APP\components\forms\FieldSelectIssues;
use APP\core\Application;
use APP\template\TemplateManager;
use PKP\components\listPanels\PKPDoiListPanel;
use PKP\core\PKPApplication;
class DoiListPanel extends PKPDoiListPanel
{
/** @var bool Whether objects being passed to DOI List Panel are submissions or not */
public $isSubmission = true;
/** @var boolean Whether to show issue filters */
public $includeIssuesFilter = false;
/**
* Add any application-specific config to the list panel setup
*/
protected function setAppConfig(array &$config): void
{
if ($this->isSubmission) {
$config['executeActionApiUrl'] = $this->doiApiUrl . '/submissions';
} else {
$config['executeActionApiUrl'] = $this->doiApiUrl . '/issues';
// Overwrite default submission published statuses for issue-specific ones
$config['publishedStatuses'] = [
'name' => 'isPublished',
'published' => 1,
'unpublished' => 0,
];
}
if ($this->includeIssuesFilter) {
$request = Application::get()->getRequest();
$issueAutosuggestField = new FieldSelectIssues('issueIds', [
'label' => __('issue.issues'),
'value' => [],
'apiUrl' => $request->getDispatcher()->url($request, PKPApplication::ROUTE_API, $request->getContext()->getPath(), 'issues'),
]);
$config['filters'][] = [
'filters' => [
[
'title' => __('issue.issues'),
'param' => 'issueIds',
'value' => [],
'filterType' => 'pkp-filter-autosuggest',
'component' => 'field-select-issues',
'autosuggestProps' => $issueAutosuggestField->getConfig(),
]
]
];
}
// Provide required locale keys
$request = Application::get()->getRequest();
$templateMgr = TemplateManager::getManager($request);
$templateMgr->setLocaleKeys([
'article.article',
'issue.issue'
]);
}
}
@@ -0,0 +1,148 @@
<?php
/**
* @file components/listPanels/SubmissionsListPanel.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionsListPanel
*
* @ingroup classes_components_listPanels
*
* @brief Instantiates and manages a UI component to list submissions.
*/
namespace APP\components\listPanels;
use APP\components\forms\FieldSelectIssues;
use APP\core\Application;
use APP\facades\Repo;
use PKP\components\forms\FieldAutosuggestPreset;
use PKP\components\listPanels\PKPSubmissionsListPanel;
class SubmissionsListPanel extends PKPSubmissionsListPanel
{
/** @var bool Whether to show inactive section filters */
public $includeActiveSectionFiltersOnly = false;
/** @var bool Whether to show issue filters */
public $includeIssuesFilter = false;
/**
* @copydoc PKPSubmissionsListPanel::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$request = Application::get()->getRequest();
if ($request->getContext()) {
$config['filters'][] = $this->getSectionFilters($this->includeActiveSectionFiltersOnly);
}
if ($this->includeIssuesFilter) {
$issueAutosuggestField = new FieldSelectIssues('issueIds', [
'label' => __('issue.issues'),
'value' => [],
'apiUrl' => $request->getDispatcher()->url($request, Application::ROUTE_API, $request->getContext()->getPath(), 'issues'),
]);
$config['filters'][] = [
'filters' => [
[
'title' => __('issue.issues'),
'param' => 'issueIds',
'value' => [],
'filterType' => 'pkp-filter-autosuggest',
'component' => 'field-select-issues',
'autosuggestProps' => $issueAutosuggestField->getConfig(),
]
]
];
}
return $config;
}
/**
* Get an array of workflow stages supported by the current app
*
* @return array
*/
public function getWorkflowStages()
{
return [
[
'param' => 'stageIds',
'value' => WORKFLOW_STAGE_ID_SUBMISSION,
'title' => __('manager.publication.submissionStage'),
],
[
'param' => 'stageIds',
'value' => WORKFLOW_STAGE_ID_EXTERNAL_REVIEW,
'title' => __('manager.publication.reviewStage'),
],
[
'param' => 'stageIds',
'value' => WORKFLOW_STAGE_ID_EDITING,
'title' => __('submission.copyediting'),
],
[
'param' => 'stageIds',
'value' => WORKFLOW_STAGE_ID_PRODUCTION,
'title' => __('manager.publication.productionStage'),
],
];
}
/**
* Compile the sections for passing as filters
*
* @param bool $excludeInactive show inactive section filters or not
*
* @return array
*/
public function getSectionFilters($excludeInactive = false)
{
$request = Application::get()->getRequest();
$context = $request->getContext();
$sections = Repo::section()->getSectionList($context->getId(), $excludeInactive);
// Use an autosuggest field if the list of submissions is too long
if (count($sections) > 5) {
$autosuggestField = new FieldAutosuggestPreset('sectionIds', [
'label' => __('section.sections'),
'value' => [],
'options' => array_values(array_map(function ($section) {
return [
'value' => (int) $section['id'],
'label' => $section['title'],
];
}, $sections)),
]);
return [
'filters' => [
[
'title' => __('section.sections'),
'param' => 'sectionIds',
'filterType' => 'pkp-filter-autosuggest',
'component' => 'field-autosuggest-preset',
'value' => [],
'autosuggestProps' => $autosuggestField->getConfig(),
]
],
];
}
return [
'heading' => __('section.sections'),
'filters' => array_map(function ($section) {
return [
'param' => 'sectionIds',
'value' => (int) $section['id'],
'title' => $section['title'],
];
}, $sections),
];
}
}
@@ -0,0 +1,744 @@
<?php
/**
* @defgroup controllers_grid_issues Issues Grid
* The Issues Grid implements the management interface allowing editors to
* manage future and archived issues.
*/
/**
* @file controllers/grid/issues/IssueGridHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueGridHandler
*
* @ingroup controllers_grid_issues
*
* @brief Handle issues grid requests.
*/
namespace APP\controllers\grid\issues;
use APP\controllers\grid\issues\form\IssueAccessForm;
use APP\controllers\grid\issues\form\IssueForm;
use APP\controllers\grid\pubIds\form\AssignPublicIdentifiersForm;
use APP\controllers\tab\pubIds\form\PublicIdentifiersForm;
use APP\core\Application;
use APP\core\Request;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use APP\issue\Collector;
use APP\jobs\notifications\IssuePublishedNotifyUsers;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\publication\Publication;
use APP\security\authorization\OjsIssueRequiredPolicy;
use APP\submission\Submission;
use APP\template\TemplateManager;
use Illuminate\Support\Facades\Bus;
use PKP\controllers\grid\GridColumn;
use PKP\controllers\grid\GridHandler;
use PKP\core\Core;
use PKP\core\JSONMessage;
use PKP\core\PKPApplication;
use PKP\db\DAO;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\file\TemporaryFileManager;
use PKP\mail\Mailer;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\Role;
class IssueGridHandler extends GridHandler
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->addRoleAssignment(
[Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
[
'fetchGrid', 'fetchRow',
'addIssue', 'editIssue', 'editIssueData', 'updateIssue',
'uploadFile', 'deleteCoverImage',
'issueToc',
'issueGalleys',
'deleteIssue', 'publishIssue', 'unpublishIssue', 'setCurrentIssue',
'identifiers', 'updateIdentifiers', 'clearPubId', 'clearIssueObjectsPubIds',
'access', 'updateAccess',
]
);
}
//
// Implement template methods from PKPHandler
//
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
// If a signoff ID was specified, authorize it.
if ($request->getUserVar('issueId')) {
$this->addPolicy(new OjsIssueRequiredPolicy($request, $args));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* @copydoc GridHandler::initialize()
*
* @param null|mixed $args
*/
public function initialize($request, $args = null)
{
parent::initialize($request, $args);
// Grid columns.
$issueGridCellProvider = new IssueGridCellProvider();
// Issue identification
$this->addColumn(
new GridColumn(
'identification',
'issue.issue',
null,
null,
$issueGridCellProvider
)
);
$this->_addCenterColumns($issueGridCellProvider);
// Number of articles
$this->addColumn(
new GridColumn(
'numArticles',
'editor.issues.numArticles',
null,
null,
$issueGridCellProvider
)
);
}
/**
* Private function to add central columns to the grid.
* May be overridden by subclasses.
*
* @param IssueGridCellProvider $issueGridCellProvider
*/
protected function _addCenterColumns($issueGridCellProvider)
{
// Default implementation does nothing.
}
/**
* Get the row handler - override the default row handler
*
* @return IssueGridRow
*/
protected function getRowInstance()
{
return new IssueGridRow();
}
//
// Public operations
//
/**
* An action to add a new issue
*
* @param array $args
* @param Request $request
*/
public function addIssue($args, $request)
{
// Calling editIssueData with an empty ID will add
// a new issue.
return $this->editIssueData($args, $request);
}
/**
* An action to edit an issue
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function editIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$templateMgr = TemplateManager::getManager($request);
if ($issue) {
$templateMgr->assign('issueId', $issue->getId());
}
$publisherIdEnabled = in_array('issue', (array) $request->getContext()->getData('enablePublisherId'));
$pubIdPlugins = PluginRegistry::getPlugins('pubIds');
if ($publisherIdEnabled || count($pubIdPlugins)) {
$templateMgr->assign('enableIdentifiers', true);
}
return new JSONMessage(true, $templateMgr->fetch('controllers/grid/issues/issue.tpl'));
}
/**
* An action to edit an issue's identifying data
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function editIssueData($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$issueForm = new IssueForm($issue);
$issueForm->initData();
return new JSONMessage(true, $issueForm->fetch($request));
}
/**
* An action to upload an issue file. Used for issue cover images.
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function uploadFile($args, $request)
{
$user = $request->getUser();
$temporaryFileManager = new TemporaryFileManager();
$temporaryFile = $temporaryFileManager->handleUpload('uploadedFile', $user->getId());
if ($temporaryFile) {
$json = new JSONMessage(true);
$json->setAdditionalAttributes([
'temporaryFileId' => $temporaryFile->getId()
]);
return $json;
} else {
return new JSONMessage(false, __('common.uploadFailed'));
}
}
/**
* Delete an uploaded cover image.
*
* @param array $args
* `coverImage` string Filename of the cover image to be deleted.
* `issueId` int Id of the issue this cover image is attached to
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function deleteCoverImage($args, $request)
{
assert(!empty($args['coverImage']) && !empty($args['issueId']));
// Check if the passed filename matches the filename for this issue's
// cover page.
$issue = Repo::issue()->get((int) $args['issueId']);
$context = $request->getContext();
if ($issue->getJournalId() != $context->getId()) {
return new JSONMessage(false, __('editor.issues.removeCoverImageOnDifferentContextNowAllowed'));
}
$locale = Locale::getLocale();
if ($args['coverImage'] != $issue->getCoverImage($locale)) {
return new JSONMessage(false, __('editor.issues.removeCoverImageFileNameMismatch'));
}
$file = $args['coverImage'];
// Remove cover image and alt text from issue settings
$issue->setCoverImage('', $locale);
$issue->setCoverImageAltText('', $locale);
Repo::issue()->edit($issue, []);
// Remove the file
$publicFileManager = new PublicFileManager();
if ($publicFileManager->removeContextFile($issue->getJournalId(), $file)) {
$json = new JSONMessage(true);
$json->setEvent('fileDeleted');
return $json;
} else {
return new JSONMessage(false, __('editor.issues.removeCoverImageFileNotFound'));
}
}
/**
* Update an issue
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function updateIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$issueForm = new IssueForm($issue);
$issueForm->readInputData();
if ($issueForm->validate()) {
$issueForm->execute();
$notificationManager = new NotificationManager();
$notificationManager->createTrivialNotification($request->getUser()->getId());
return DAO::getDataChangedEvent();
} else {
return new JSONMessage(true, $issueForm->fetch($request));
}
}
/**
* An action to edit an issue's access settings
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function access($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$issueAccessForm = new IssueAccessForm($issue);
$issueAccessForm->initData();
return new JSONMessage(true, $issueAccessForm->fetch($request));
}
/**
* Update an issue's access settings
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function updateAccess($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$issueAccessForm = new IssueAccessForm($issue);
$issueAccessForm->readInputData();
if ($issueAccessForm->validate()) {
$issueAccessForm->execute();
$notificationManager = new NotificationManager();
$notificationManager->createTrivialNotification($request->getUser()->getId());
return DAO::getDataChangedEvent();
} else {
return new JSONMessage(true, $issueAccessForm->fetch($request));
}
}
/**
* Removes an issue
*
* @param array $args
* @param Request $request
*/
public function deleteIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
if (!$issue || !$request->checkCSRF()) {
return new JSONMessage(false);
}
$journal = $request->getJournal();
if ($issue->getJournalId() != $journal->getId()) {
return new JSONMessage(false);
}
// remove all published submissions and return original articles to editing queue
$submissions = Repo::submission()
->getCollector()
->filterByContextIds([$issue->getData('journalId')])
->filterByIssueIds([$issue->getId()])
->getMany();
foreach ($submissions as $submission) {
$publications = $submission->getData('publications');
foreach ($publications as $publication) {
if ($publication->getData('issueId') === (int) $issue->getId()) {
Repo::publication()->edit($publication, ['issueId' => '', 'status' => Submission::STATUS_QUEUED]);
}
}
$newSubmission = Repo::submission()->get($submission->getId());
Repo::submission()->updateStatus($newSubmission);
}
Repo::issue()->delete($issue);
$currentIssue = Repo::issue()->getCurrent($issue->getJournalId());
if ($currentIssue != null && $issue->getId() == $currentIssue->getId()) {
$issues = Repo::issue()->getCollector()
->filterByContextIds([$journal->getId()])
->filterByPublished(true)
->orderBy(Collector::ORDERBY_PUBLISHED_ISSUES)
->getMany();
if ($issue = $issues->first()) {
Repo::issue()->updateCurrent($journal->getId(), $issue);
}
}
return DAO::getDataChangedEvent($issue->getId());
}
/**
* An action to edit issue pub ids
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function identifiers($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$form = new PublicIdentifiersForm($issue);
$form->initData();
return new JSONMessage(true, $form->fetch($request));
}
/**
* Update issue pub ids
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function updateIdentifiers($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$form = new PublicIdentifiersForm($issue);
$form->readInputData();
if ($form->validate()) {
$form->execute();
return DAO::getDataChangedEvent($issue->getId());
} else {
return new JSONMessage(true, $form->fetch($request));
}
}
/**
* Clear issue pub id
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function clearPubId($args, $request)
{
if (!$request->checkCSRF()) {
return new JSONMessage(false);
}
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$form = new PublicIdentifiersForm($issue);
$form->clearPubId($request->getUserVar('pubIdPlugIn'));
$json = new JSONMessage(true);
$json->setEvent('reloadTab', [['tabsSelector' => '#editIssueTabs', 'tabSelector' => '#identifiersTab']]);
return $json;
}
/**
* Clear issue objects pub ids
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function clearIssueObjectsPubIds($args, $request)
{
if (!$request->checkCSRF()) {
return new JSONMessage(false);
}
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$form = new PublicIdentifiersForm($issue);
$form->clearIssueObjectsPubIds($request->getUserVar('pubIdPlugIn'));
return new JSONMessage(true);
}
/**
* Display the table of contents
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function issueToc($args, $request)
{
$templateMgr = TemplateManager::getManager($request);
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$templateMgr->assign('issue', $issue);
return new JSONMessage(true, $templateMgr->fetch('controllers/grid/issues/issueToc.tpl'));
}
/**
* Displays the issue galleys page.
*
* @param array $args
* @param Request $request
*
* @return JSONMessage JSON object
*/
public function issueGalleys($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$templateMgr = TemplateManager::getManager($request);
$dispatcher = $request->getDispatcher();
return $templateMgr->fetchAjax(
'issueGalleysGridContainer',
$dispatcher->url(
$request,
PKPApplication::ROUTE_COMPONENT,
null,
'grid.issueGalleys.IssueGalleyGridHandler',
'fetchGrid',
null,
['issueId' => $issue->getId()]
)
);
}
/**
* Publish issue
*
* @param array $args
* @param Request $request
*/
public function publishIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$context = $request->getContext();
$contextId = $context->getId();
$wasPublished = $issue->getPublished();
if (!$wasPublished) {
$confirmationText = __('editor.issues.confirmPublish');
$formTemplate = $this->getAssignPublicIdentifiersFormTemplate();
$assignPublicIdentifiersForm = new AssignPublicIdentifiersForm($formTemplate, $issue, true, $confirmationText);
if (!$request->getUserVar('confirmed')) {
// Display assign pub ids modal
$assignPublicIdentifiersForm->initData();
return new JSONMessage(true, $assignPublicIdentifiersForm->fetch($request));
}
// Assign pub ids
$assignPublicIdentifiersForm->readInputData();
if (!$assignPublicIdentifiersForm->validate()) {
return new JSONMessage(true, $assignPublicIdentifiersForm->fetch($request));
}
$assignPublicIdentifiersForm->execute();
Repo::issue()->createDoi($issue);
}
if (!$request->checkCSRF()) {
return new JSONMessage(false);
}
$issue->setPublished(1);
$issue->setDatePublished(Core::getCurrentDate());
// If subscriptions with delayed open access are enabled then
// update open access date according to open access delay policy
if ($context->getData('publishingMode') == \APP\journal\Journal::PUBLISHING_MODE_SUBSCRIPTION && ($delayDuration = $context->getData('delayedOpenAccessDuration'))) {
$delayYears = (int)floor($delayDuration / 12);
$delayMonths = (int)fmod($delayDuration, 12);
$curYear = date('Y');
$curMonth = date('n');
$curDay = date('j');
$delayOpenAccessYear = $curYear + $delayYears + (int)floor(($curMonth + $delayMonths) / 12);
$delayOpenAccessMonth = (int)fmod($curMonth + $delayMonths, 12);
$issue->setAccessStatus(\APP\issue\Issue::ISSUE_ACCESS_SUBSCRIPTION);
$issue->setOpenAccessDate(date('Y-m-d H:i:s', mktime(0, 0, 0, $delayOpenAccessMonth, $curDay, $delayOpenAccessYear)));
}
Hook::call('IssueGridHandler::publishIssue', [&$issue]);
Repo::issue()->updateCurrent($contextId, $issue);
if (!$wasPublished) {
Repo::doi()->issueUpdated($issue);
// Publish all related publications
// Include published submissions in order to support cases where two
// versions of the same submission are published in distinct issues. In
// such cases, the submission will be STATUS_PUBLISHED but the
// publication will be STATUS_SCHEDULED.
$submissions = Repo::submission()->getCollector()
->filterByContextIds([$issue->getJournalId()])
->filterByIssueIds([$issue->getId()])
->filterByStatus([Submission::STATUS_SCHEDULED, Submission::STATUS_PUBLISHED])
->getMany();
foreach ($submissions as $submission) { /** @var Submission $submission */
$publications = $submission->getData('publications');
foreach ($publications as $publication) { /** @var Publication $publication */
if ($publication->getData('status') === Submission::STATUS_SCHEDULED && $publication->getData('issueId') === (int) $issue->getId()) {
Repo::publication()->publish($publication);
}
}
}
}
// Send a notification to associated users if selected and context is publishing content online with OJS
if ($request->getUserVar('sendIssueNotification') && $context->getData('publishingMode') != \APP\journal\Journal::PUBLISHING_MODE_NONE) {
// Notify users
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[Notification::NOTIFICATION_TYPE_PUBLISHED_ISSUE],
[$contextId]
);
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[
NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY,
NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY
],
[Notification::NOTIFICATION_TYPE_PUBLISHED_ISSUE],
[$contextId]
);
$userIdsToNotifyAndMail = $userIdsToNotify->intersect($userIdsToMail);
$userIdsToNotify = $userIdsToNotify->diff($userIdsToMail);
$jobs = [];
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$jobs[] = new IssuePublishedNotifyUsers(
$notifyUserIds,
$contextId,
$issue,
Locale::getLocale(),
);
}
foreach ($userIdsToNotifyAndMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $mailUserIds) {
$jobs[] = new IssuePublishedNotifyUsers(
$mailUserIds,
$contextId,
$issue,
Locale::getLocale(),
$request->getUser()
);
}
Bus::batch($jobs)->dispatch();
}
$json = DAO::getDataChangedEvent();
$json->setGlobalEvent('issuePublished', ['id' => $issue->getId()]);
return $json;
}
/**
* Unpublish a previously-published issue
*
* @param array $args
* @param Request $request
*/
public function unpublishIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$journal = $request->getJournal();
if (!$request->checkCSRF()) {
return new JSONMessage(false);
}
// NB: Data set via params because setData('datePublished', null)
// removes the entry into _data rather than updating 'datePublished' to null.
$updateParams = [
'published' => 0,
'datePublished' => null
];
Hook::call('IssueGridHandler::unpublishIssue', [&$issue]);
Repo::issue()->edit($issue, $updateParams);
Repo::issue()->updateCurrent($request->getContext()->getId());
Repo::doi()->issueUpdated($issue);
// insert article tombstones for all articles
$submissions = Repo::submission()->getCollector()
->filterByContextIds([$issue->getJournalId()])
->filterByIssueIds([$issue->getId()])
->getMany();
foreach ($submissions as $submission) { /** @var Submission $submission */
$publications = $submission->getData('publications');
foreach ($publications as $publication) { /** @var Publication $publication */
if ($publication->getData('status') === Submission::STATUS_PUBLISHED && $publication->getData('issueId') === (int) $issue->getId()) {
// Republish the publication in the issue, now that it's status has changed,
// to ensure the publication's status is restored to Submission::STATUS_SCHEDULED
// rather than Submission::STATUS_QUEUED
Repo::publication()->unpublish($publication);
Repo::publication()->publish($publication);
}
}
}
$json = DAO::getDataChangedEvent($issue->getId());
$json->setGlobalEvent('issueUnpublished', ['id' => $issue->getId()]);
return $json;
}
/**
* Set Issue as current
*
* @param array $args
* @param Request $request
*/
public function setCurrentIssue($args, $request)
{
$issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE);
$journal = $request->getJournal();
if (!$request->checkCSRF()) {
return new JSONMessage(false);
}
Repo::issue()->updateCurrent($journal->getId(), $issue);
$dispatcher = $request->getDispatcher();
return DAO::getDataChangedEvent();
}
/**
* Get the template for the assign public identifiers form.
*
* @return string
*/
public function getAssignPublicIdentifiersFormTemplate()
{
return 'controllers/grid/pubIds/form/assignPublicIdentifiersForm.tpl';
}
}
@@ -0,0 +1,25 @@
<?php
/**
* @file controllers/grid/users/author/form/AuthorForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AuthorForm
*
* @ingroup controllers_grid_users_author_form
*
* @deprecated 3.4
*
* @brief Form for adding/editing a author
*/
namespace APP\controllers\grid\users\author\form;
use PKP\controllers\grid\users\author\form\PKPAuthorForm;
class AuthorForm extends PKPAuthorForm
{
}
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* @file classes/core/AppServiceProvider.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AppServiceProvider
*
* @ingroup core
*
* @brief Resolves requests for application classes such as the request handler
* to support dependency injection
*/
namespace APP\core;
use PKP\core\PKPRequest;
class AppServiceProvider extends \PKP\core\AppServiceProvider
{
/**
* @copydoc \PKP\core\AppServiceProvider::register()
*/
public function register()
{
parent::register();
$this->app->bind(Request::class, PKPRequest::class);
}
}
+261
View File
@@ -0,0 +1,261 @@
<?php
/**
* @file classes/core/Application.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Application
*
* @ingroup core
*
* @see PKPApplication
*
* @brief Class describing this application.
*
*/
namespace APP\core;
use APP\facades\Repo;
use APP\journal\JournalDAO;
use APP\payment\ojs\OJSPaymentManager;
use PKP\context\Context;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\security\Role;
use PKP\submission\RepresentationDAOInterface;
class Application extends PKPApplication
{
public const ASSOC_TYPE_ARTICLE = self::ASSOC_TYPE_SUBMISSION; // DEPRECATED but needed by filter framework;
public const ASSOC_TYPE_GALLEY = self::ASSOC_TYPE_REPRESENTATION;
public const ASSOC_TYPE_JOURNAL = 0x0000100;
public const ASSOC_TYPE_ISSUE = 0x0000103;
public const ASSOC_TYPE_ISSUE_GALLEY = 0x0000105;
public const CONTEXT_JOURNAL = 1; // not used?
public const REQUIRES_XSL = false;
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
if (!PKP_STRICT_MODE) {
foreach ([
'REQUIRES_XSL',
'ASSOC_TYPE_ARTICLE',
'ASSOC_TYPE_GALLEY',
'ASSOC_TYPE_JOURNAL',
'ASSOC_TYPE_ISSUE',
'ASSOC_TYPE_ISSUE_GALLEY',
'CONTEXT_JOURNAL',
] as $constantName) {
if (!defined($constantName)) {
define($constantName, constant('self::' . $constantName));
}
}
if (!class_exists('\Application')) {
class_alias('\APP\core\Application', '\Application');
}
}
// Add application locales
Locale::registerPath(BASE_SYS_DIR . '/locale');
}
/**
* Get the name of the application context.
*/
public function getContextName(): string
{
return 'journal';
}
/**
* Get the symbolic name of this application
*
* @return string
*/
public static function getName()
{
return 'ojs2';
}
/**
* Get the locale key for the name of this application.
*
* @return string
*/
public function getNameKey()
{
return('common.software');
}
/**
* Get the URL to the XML descriptor for the current version of this
* application.
*
* @return string
*/
public function getVersionDescriptorUrl()
{
return 'https://pkp.sfu.ca/ojs/xml/ojs-version.xml';
}
/**
* Get the map of DAOName => full.class.Path for this application.
*
* @return array
*/
public function getDAOMap()
{
return array_merge(parent::getDAOMap(), [
'ArticleSearchDAO' => 'APP\search\ArticleSearchDAO',
'IndividualSubscriptionDAO' => 'APP\subscription\IndividualSubscriptionDAO',
'InstitutionalSubscriptionDAO' => 'APP\subscription\InstitutionalSubscriptionDAO',
'IssueGalleyDAO' => 'APP\issue\IssueGalleyDAO',
'IssueFileDAO' => 'APP\issue\IssueFileDAO',
'JournalDAO' => 'APP\journal\JournalDAO',
'MetricsDAO' => 'APP\statistics\MetricsDAO',
'OAIDAO' => 'APP\oai\ojs\OAIDAO',
'OJSCompletedPaymentDAO' => 'APP\payment\ojs\OJSCompletedPaymentDAO',
'SubscriptionDAO' => 'APP\subscription\SubscriptionDAO',
'SubscriptionTypeDAO' => 'APP\subscription\SubscriptionTypeDAO',
'TemporaryTotalsDAO' => 'APP\statistics\TemporaryTotalsDAO',
'TemporaryItemInvestigationsDAO' => 'APP\statistics\TemporaryItemInvestigationsDAO',
'TemporaryItemRequestsDAO' => 'APP\statistics\TemporaryItemRequestsDAO',
]);
}
/**
* Get the list of plugin categories for this application.
*
* @return array
*/
public function getPluginCategories()
{
return [
// NB: Meta-data plug-ins are first in the list as this
// will make them load (and install) first.
// This is necessary as several other plug-in categories
// depend on meta-data. This is a very rudimentary type of
// dependency management for plug-ins.
'metadata',
'blocks',
'gateways',
'generic',
'importexport',
'oaiMetadataFormats',
'paymethod',
'pubIds',
'reports',
'themes'
];
}
/**
* Get the top-level context DAO.
*
* @return JournalDAO
*/
public static function getContextDAO()
{
/** @var JournalDAO */
$dao = DAORegistry::getDAO('JournalDAO');
return $dao;
}
/**
* Get the representation DAO.
*
* @return \PKP\galley\DAO&RepresentationDAOInterface
*/
public static function getRepresentationDAO(): RepresentationDAOInterface
{
return Repo::galley()->dao;
}
/**
* Get a SubmissionSearchIndex instance.
*/
public static function getSubmissionSearchIndex()
{
return new \APP\search\ArticleSearchIndex();
}
/**
* Get a SubmissionSearchDAO instance.
*/
public static function getSubmissionSearchDAO()
{
return DAORegistry::getDAO('ArticleSearchDAO');
}
/**
* Get the stages used by the application.
*
* @return array
*/
public static function getApplicationStages()
{
// We leave out WORKFLOW_STAGE_ID_PUBLISHED since it technically is not a 'stage'.
return [
WORKFLOW_STAGE_ID_SUBMISSION,
WORKFLOW_STAGE_ID_EXTERNAL_REVIEW,
WORKFLOW_STAGE_ID_EDITING,
WORKFLOW_STAGE_ID_PRODUCTION
];
}
/**
* Returns the context type for this application.
*
* @return int Application::ASSOC_TYPE_...
*/
public static function getContextAssocType()
{
return self::ASSOC_TYPE_JOURNAL;
}
/**
* Get the file directory array map used by the application.
*/
public static function getFileDirectories()
{
return ['context' => '/journals/', 'submission' => '/articles/'];
}
/**
* @copydoc PKPApplication::getRoleNames()
*
* @param null|mixed $roleIds
*/
public static function getRoleNames($contextOnly = false, $roleIds = null)
{
$roleNames = parent::getRoleNames($contextOnly, $roleIds);
if (!$roleIds || in_array(Role::ROLE_ID_SUBSCRIPTION_MANAGER, $roleIds)) {
$roleNames[Role::ROLE_ID_SUBSCRIPTION_MANAGER] = 'user.role.subscriptionManager';
}
return $roleNames;
}
/**
* Get the payment manager.
*
* @param \APP\journal\Journal $context
*
* @return OJSPaymentManager
*/
public static function getPaymentManager($context)
{
return new OJSPaymentManager($context);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* @file classes/core/PageRouter.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PageRouter
*
* @ingroup core
*
* @brief Class providing OJS-specific page routing.
*/
namespace APP\core;
class PageRouter extends \PKP\core\PKPPageRouter
{
/**
* get the cacheable pages
*
* @return array
*/
public function getCacheablePages()
{
return ['about', 'announcement', 'help', 'index', 'information', 'issue', ''];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\core\PageRouter', '\PageRouter');
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/core/Request.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Request
*
* @ingroup core
*
* @brief @verbatim Class providing operations associated with HTTP requests.
* Requests are assumed to be in the format http://host.tld/index.php/<journal_id>/<page_name>/<operation_name>/<arguments...>
* <journal_id> is assumed to be "index" for top-level site requests. @endverbatim
*/
namespace APP\core;
use APP\journal\Journal;
use PKP\core\PKPRequest;
class Request extends PKPRequest
{
/**
* @see PKPPageRouter::getContext()
*/
public function getJournal(): ?Journal
{
return $this->getContext();
}
/**
* Deprecated
*
* @see PKPPageRouter::getContext()
*/
public function getContext(): ?Journal
{
return parent::getContext();
}
/**
* Deprecated
*
* @see PKPPageRouter::url()
*
* @param null|mixed $journalPath
* @param null|mixed $page
* @param null|mixed $op
* @param null|mixed $path
* @param null|mixed $params
* @param null|mixed $anchor
*/
public function url(
$journalPath = null,
$page = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
return $this->_delegateToRouter(
'url',
$journalPath,
$page,
$op,
$path,
$params,
$anchor,
$escape
);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* @file classes/core/Services.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Services
*
* @ingroup core
*
* @see Core
*
* @brief Pimple Dependency Injection Container.
*/
namespace APP\core;
use APP\services\OJSServiceProvider;
class Services extends \PKP\core\PKPServices
{
/**
* container initialization
*/
protected function init()
{
$this->container->register(new OJSServiceProvider());
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\core\Services', '\Services');
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/**
* @defgroup decision Decision
*/
/**
* @file classes/decision/Decision.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Decision
*
* @ingroup decision
*
* @see DAO
*
* @brief An editorial decision taken on a submission, such as to accept, decline or request revisions.
*/
namespace APP\decision;
use PKP\decision\Decision as BaseDecision;
class Decision extends BaseDecision
{
}
if (!PKP_STRICT_MODE) {
// Some constants are not redefined here because they never existed as global constants
define('SUBMISSION_EDITOR_DECISION_EXTERNAL_REVIEW', Decision::EXTERNAL_REVIEW);
define('SUBMISSION_EDITOR_DECISION_ACCEPT', Decision::ACCEPT);
define('SUBMISSION_EDITOR_DECISION_DECLINE', Decision::DECLINE);
define('SUBMISSION_EDITOR_DECISION_PENDING_REVISIONS', Decision::PENDING_REVISIONS);
define('SUBMISSION_EDITOR_DECISION_RESUBMIT', Decision::RESUBMIT);
}
+100
View File
@@ -0,0 +1,100 @@
<?php
/**
* @file classes/decision/Repository.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage editorial decisions.
*/
namespace APP\decision;
use APP\decision\types\Accept;
use APP\decision\types\SkipExternalReview;
use APP\notification\Notification;
use Illuminate\Database\Eloquent\Collection;
use PKP\decision\types\BackFromCopyediting;
use PKP\decision\types\BackFromProduction;
use PKP\decision\types\CancelReviewRound;
use PKP\decision\types\Decline;
use PKP\decision\types\InitialDecline;
use PKP\decision\types\NewExternalReviewRound;
use PKP\decision\types\RecommendAccept;
use PKP\decision\types\RecommendDecline;
use PKP\decision\types\RecommendResubmit;
use PKP\decision\types\RecommendRevisions;
use PKP\decision\types\RequestRevisions;
use PKP\decision\types\Resubmit;
use PKP\decision\types\RevertDecline;
use PKP\decision\types\RevertInitialDecline;
use PKP\decision\types\SendExternalReview;
use PKP\decision\types\SendToProduction;
use PKP\plugins\Hook;
class Repository extends \PKP\decision\Repository
{
/** The valid decision types */
protected ?Collection $decisionTypes;
public function getDecisionTypes(): Collection
{
if (!isset($this->decisionTypes)) {
$decisionTypes = new Collection([
new Accept(),
new Decline(),
new InitialDecline(),
new NewExternalReviewRound(),
new RecommendAccept(),
new RecommendDecline(),
new RecommendResubmit(),
new RecommendRevisions(),
new Resubmit(),
new RequestRevisions(),
new RevertDecline(),
new RevertInitialDecline(),
new SendExternalReview(),
new SendToProduction(),
new SkipExternalReview(),
new BackFromProduction(),
new BackFromCopyediting(),
new CancelReviewRound(),
]);
Hook::call('Decision::types', [$decisionTypes]);
$this->decisionTypes = $decisionTypes;
}
return $this->decisionTypes;
}
public function getDeclineDecisionTypes(): array
{
return [
new InitialDecline(),
new Decline(),
];
}
protected function getReviewNotificationTypes(): array
{
return [Notification::NOTIFICATION_TYPE_PENDING_EXTERNAL_REVISIONS];
}
public function getDecisionTypesMadeByRecommendingUsers(int $stageId): array
{
$recommendatorsAvailableDecisions = [];
switch($stageId) {
case WORKFLOW_STAGE_ID_SUBMISSION:
$recommendatorsAvailableDecisions = [
new SendExternalReview()
];
}
Hook::call('Workflow::RecommendatorDecisions', [&$recommendatorsAvailableDecisions, $stageId]);
return $recommendatorsAvailableDecisions;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
/**
* @file classes/decision/types/Accept.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Accept
*
* @brief Extend the Accept decision to support APC payments
*/
namespace APP\decision\types;
use APP\core\Application;
use APP\decision\Decision;
use APP\decision\types\traits\RequestPayment;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\Steps;
use PKP\decision\types\Accept as TypesAccept;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class Accept extends TypesAccept
{
use RequestPayment;
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_PAYMENT:
$this->validatePaymentAction($action, $actionErrorKey, $validator, $context);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case self::ACTION_PAYMENT:
$this->requestPayment($submission, $editor, $context);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = parent::getSteps($submission, $context, $editor, $reviewRound);
// Request payment if configured
$paymentManager = Application::getPaymentManager($context);
if ($paymentManager->publicationEnabled()) {
$steps->addStep($this->getPaymentForm($context), true);
}
return $steps;
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @file classes/decision/types/SkipExternalReview.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SkipExternalReview
*
* @brief Extend the skip review decision to handle APC payments.
*/
namespace APP\decision\types;
use APP\core\Application;
use APP\decision\Decision;
use APP\decision\types\traits\RequestPayment;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\Steps;
use PKP\decision\types\SkipExternalReview as PKPSkipExternalReview;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class SkipExternalReview extends PKPSkipExternalReview
{
use RequestPayment;
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case self::ACTION_PAYMENT:
$this->validatePaymentAction($action, $actionErrorKey, $validator, $context);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case self::ACTION_PAYMENT:
$this->requestPayment($submission, $editor, $context);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = parent::getSteps($submission, $context, $editor, $reviewRound);
// Request payment if configured
$paymentManager = Application::getPaymentManager($context);
if ($paymentManager->publicationEnabled()) {
$steps->addStep($this->getPaymentForm($context), true);
}
return $steps;
}
}
@@ -0,0 +1,100 @@
<?php
/**
* @file classes/decision/types/traits/RequestPayment.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions that may request a payment
*/
namespace APP\decision\types\traits;
use APP\components\forms\decision\RequestPaymentDecisionForm;
use APP\core\Application;
use APP\facades\Repo;
use APP\mail\mailables\PaymentRequest;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\payment\ojs\OJSPaymentManager;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\steps\Form;
use PKP\user\User;
trait RequestPayment
{
protected string $ACTION_PAYMENT = 'payment';
/**
* Get the form to request or waive payment
*/
protected function getPaymentForm(Context $context): Form
{
return new Form(
$this->ACTION_PAYMENT,
__('editor.article.payment.requestPayment'),
'',
new RequestPaymentDecisionForm($context)
);
}
/**
* Validate the decision action to request or waive payment
*/
protected function validatePaymentAction(array $action, string $actionErrorKey, Validator $validator, Context $context)
{
$paymentManager = Application::getPaymentManager($context);
if (!$paymentManager->publicationEnabled()) {
$validator->errors()->add($actionErrorKey . '.requestPayment', __('payment.requestPublicationFee.notEnabled'));
} elseif (!isset($action['requestPayment'])) {
$validator->errors()->add($actionErrorKey . '.requestPayment', __('validator.required'));
}
}
/**
* Request payment from authors
*/
protected function requestPayment(Submission $submission, User $editor, Context $context)
{
$paymentManager = Application::getPaymentManager($context);
$queuedPayment = $paymentManager->createQueuedPayment(
Application::get()->getRequest(),
OJSPaymentManager::PAYMENT_TYPE_PUBLICATION,
$editor->getId(),
$submission->getId(),
$context->getData('publicationFee'),
$context->getData('currency')
);
$paymentManager->queuePayment($queuedPayment);
// Notify authors that this needs payment.
$notificationMgr = new NotificationManager();
$authorIds = $this->getAssignedAuthorIds($submission);
foreach ($authorIds as $authorId) {
$notificationMgr->createNotification(
Application::get()->getRequest(),
$authorId,
Notification::NOTIFICATION_TYPE_PAYMENT_REQUIRED,
$context->getId(),
Application::ASSOC_TYPE_QUEUED_PAYMENT,
$queuedPayment->getId(),
Notification::NOTIFICATION_LEVEL_TASK
);
$mailable = new PaymentRequest($context, $submission, $queuedPayment);
$template = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$mailable->from($context->getData('contactEmail'), $context->getData('contactName'))
->recipients([Repo::user()->get($authorId)])
->subject($template->getLocalizedData('subject'))
->body($template->getLocalizedData('body'));
Mail::send($mailable);
}
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* @file classes/doi/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @ingroup doi
*
* @see Doi
*
* @brief Operations for retrieving and modifying Doi objects.
*/
namespace APP\doi;
use APP\facades\Repo;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use PKP\context\Context;
use PKP\doi\Doi;
use PKP\submission\PKPSubmission;
class DAO extends \PKP\doi\DAO
{
/**
* Gets all depositable submission IDs along with all associated DOI IDs for use in DOI bulk deposit jobs.
* This method is used to collect all valid submissions/IDs in a single query specifically for use with
* queued jobs for depositing DOIs with a registration agency.
*
*/
public function getAllDepositableSubmissionIds(Context $context): Collection
{
$enabledDoiTypes = $context->getData(Context::SETTING_ENABLED_DOI_TYPES) ?? [];
$q = DB::table($this->table, 'd')
->leftJoin('publications as p', 'd.doi_id', '=', 'p.doi_id')
->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id')
->where('d.context_id', '=', $context->getId())
->where(function (Builder $q) use ($enabledDoiTypes) {
// Publication DOIs
$q->when(in_array(Repo::doi()::TYPE_PUBLICATION, $enabledDoiTypes), function (Builder $q) {
$q->whereIn('d.doi_id', function (Builder $q) {
$q->select('p.doi_id')
->from('publications', 'p')
->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id')
->whereColumn('p.publication_id', '=', 's.current_publication_id')
->whereNotNull('p.doi_id')
->where('p.status', '=', PKPSubmission::STATUS_PUBLISHED);
});
})
// Galley DOIs
->when(in_array(Repo::doi()::TYPE_REPRESENTATION, $enabledDoiTypes), function (Builder $q) {
$q->orWhereIn('d.doi_id', function (Builder $q) {
$q->select('g.doi_id')
->from('publication_galleys', 'g')
->leftJoin('publications as p', 'g.publication_id', '=', 'p.publication_id')
->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id')
->whereColumn('p.publication_id', '=', 's.current_publication_id')
->whereNotNull('g.doi_id')
->where('p.status', '=', PKPSubmission::STATUS_PUBLISHED);
});
});
});
$q->whereIn('d.status', [Doi::STATUS_UNREGISTERED, Doi::STATUS_ERROR, Doi::STATUS_STALE]);
return $q->get(['s.submission_id', 'd.doi_id']);
}
/**
* Gets all depositable issues IDs along with all associated DOI IDs for use in DOI bulk deposit jobs.
* This method is used to collect all valid issues/IDs in a single query specifically for use with
* queued jobs for depositing DOIs with a registration agency.
*
*/
public function getAllDepositableIssueIds(Context $context): Collection
{
$q = DB::table($this->table, 'd')
->leftJoin('issues as i', 'i.doi_id', '=', 'd.doi_id')
->where('i.journal_id', '=', $context->getId())
->whereNotNull('i.doi_id')
->where('i.published', '=', 1)
->whereIn('d.status', [Doi::STATUS_UNREGISTERED, Doi::STATUS_ERROR, Doi::STATUS_STALE]);
return $q->get(['i.issue_id', 'i.doi_id']);
}
}
+344
View File
@@ -0,0 +1,344 @@
<?php
/**
* @file classes/doi/Repository.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage DOIs.
*/
namespace APP\doi;
use APP\core\Request;
use APP\facades\Repo;
use APP\issue\Issue;
use APP\jobs\doi\DepositIssue;
use APP\journal\Journal;
use APP\journal\JournalDAO;
use APP\plugins\PubIdPlugin;
use APP\publication\Publication;
use APP\submission\Submission;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use PKP\context\Context;
use PKP\core\DataObject;
use PKP\db\DAORegistry;
use PKP\doi\Collector;
use PKP\doi\exceptions\DoiException;
use PKP\galley\Galley;
use PKP\services\PKPSchemaService;
use PKP\submission\Representation;
class Repository extends \PKP\doi\Repository
{
public const TYPE_ISSUE = 'issue';
public const CUSTOM_ISSUE_PATTERN = 'doiIssueSuffixPattern';
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
parent::__construct($dao, $request, $schemaService);
}
public function getCollector(): Collector
{
return App::makeWith(Collector::class, ['dao' => $this->dao]);
}
/**
* Create a DOI for the given publication.
*
* @throws DoiException
*/
public function mintPublicationDoi(Publication $publication, Submission $submission, Context $context): int
{
// Default suffix does not rely on any other metadata
if ($context->getData(Context::SETTING_DOI_SUFFIX_TYPE) === Repo::doi()::SUFFIX_DEFAULT) {
return $this->mintAndStoreDoi($context, $this->generateDefaultSuffix());
}
// If not using default suffix, additional checks are required
$issueId = $publication->getData('issueId');
if ($issueId === null) {
throw new DoiException(
DoiException::PUBLICATION_MISSING_ISSUE,
$submission->getCurrentPublication()->getLocalizedFullTitle(),
$publication->getLocalizedFullTitle()
);
}
$issue = Repo::issue()->get($publication->getData('issueId'));
if ($issue === null) {
throw new DoiException(
DoiException::PUBLICATION_MISSING_ISSUE,
$submission->getCurrentPublication()->getLocalizedFullTitle(),
$publication->getLocalizedFullTitle()
);
} elseif ($issue && $context->getId() != $issue->getJournalId()) {
throw new DoiException(
DoiException::PUBLICATION_MISSING_ISSUE,
$submission->getCurrentPublication()->getLocalizedFullTitle(),
$publication->getLocalizedFullTitle()
);
}
$doiSuffix = $this->generateSuffixPattern($publication, $context, $context->getData(Context::SETTING_DOI_SUFFIX_TYPE), $issue, $submission);
return $this->mintAndStoreDoi($context, $doiSuffix);
}
/**
* Create a DOI for the given galley
*
* @throws DoiException
*/
public function mintGalleyDoi(Galley $galley, Publication $publication, Submission $submission, Context $context): int
{
// Default suffix does not rely on any other metadata
if ($context->getData(Context::SETTING_DOI_SUFFIX_TYPE) === Repo::doi()::SUFFIX_DEFAULT) {
return $this->mintAndStoreDoi($context, $this->generateDefaultSuffix());
}
// If not using default suffix, additional checks are required
$issue = Repo::issue()->getBySubmissionId($submission->getId());
if ($issue === null) {
throw new DoiException(
DoiException::REPRESENTATION_MISSING_ISSUE,
$submission->getCurrentPublication()->getLocalizedFullTitle(),
$galley->getLabel()
);
} elseif ($issue && $context->getId() != $issue->getJournalId()) {
throw new DoiException(
DoiException::REPRESENTATION_MISSING_ISSUE,
$submission->getCurrentPublication()->getLocalizedFullTitle(),
$galley->getLabel()
);
}
$doiSuffix = $this->generateSuffixPattern($galley, $context, $context->getData(Context::SETTING_DOI_SUFFIX_TYPE), $issue, $submission, $galley);
return $this->mintAndStoreDoi($context, $doiSuffix);
}
/**
* Create a DOI for the given Issue
*
* @throws DoiException
*/
public function mintIssueDoi(Issue $issue, Context $context): int
{
if ($context->getId() != $issue->getJournalId()) {
throw new DoiException(
DoiException::INCORRECT_ISSUE_CONTEXT,
$issue->getLocalizedTitle(),
$issue->getLocalizedTitle()
);
}
// Default suffix does not rely on any other metadata
if ($context->getData(Context::SETTING_DOI_SUFFIX_TYPE) === Repo::doi()::SUFFIX_DEFAULT) {
return $this->mintAndStoreDoi($context, $this->generateDefaultSuffix());
}
// If not using default suffix, use pattern generator
$doiSuffix = $this->generateSuffixPattern($issue, $context, $context->getData(Context::SETTING_DOI_SUFFIX_TYPE), $issue);
return $this->mintAndStoreDoi($context, $doiSuffix);
}
/**
* Handles updating issue DOI status when metadata changes
*
*/
public function issueUpdated(Issue $issue)
{
$doiIds = Repo::doi()->getDoisForIssue($issue->getId());
$this->dao->markStale($doiIds);
}
/**
* Generate a suffix using a provided pattern type
*
* @param string $patternType Repo::doi()::CUSTOM_SUFFIX_* constants
*
*/
protected function generateSuffixPattern(
DataObject $object,
Context $context,
string $patternType,
?Issue $issue = null,
?Submission $submission = null,
?Representation $representation = null
): string {
$doiSuffix = '';
switch ($patternType) {
case self::SUFFIX_CUSTOM_PATTERN:
$pubIdSuffixPattern = $this->getPubIdSuffixPattern($object, $context);
$doiSuffix = PubIdPlugin::generateCustomPattern($context, $pubIdSuffixPattern, $object, $issue, $submission, $representation);
break;
case self::SUFFIX_MANUAL:
break;
}
return $doiSuffix;
}
/**
* Gets all DOI IDs related to a submission
*
* @return array<int> DOI IDs
*/
public function getDoisForSubmission(int $submissionId): array
{
$doiIds = Collection::make();
$submission = Repo::submission()->get($submissionId);
/** @var Publication[] $publications */
$publications = $submission->getData('publications');
/** @var JournalDAO $contextDao */
$contextDao = DAORegistry::getDAO('JournalDAO');
/** @var Journal $context */
$context = $contextDao->getById($submission->getData('contextId'));
foreach ($publications as $publication) {
$publicationDoiId = $publication->getData('doiId');
if (!empty($publicationDoiId) && $context->isDoiTypeEnabled(self::TYPE_PUBLICATION)) {
$doiIds->add($publicationDoiId);
}
// Galleys
$galleys = Repo::galley()->getCollector()
->filterByPublicationIds(['publicationIds' => $publication->getId()])
->getMany();
foreach ($galleys as $galley) {
$galleyDoiId = $galley->getData('doiId');
if (!empty($galleyDoiId) && $context->isDoiTypeEnabled(self::TYPE_REPRESENTATION)) {
$doiIds->add($galleyDoiId);
}
}
}
return $doiIds->unique()->toArray();
}
/**
* Gets all DOIs associated with an issue
* NB: Assumes only enabled DOI types are allowed
*
* @param bool $enabledDoiTypesOnly
*
* @throws \Exception
*
* @return array<int> DOI IDs
*
*/
public function getDoisForIssue(int $issueId, $enabledDoiTypesOnly = false): array
{
$doiIds = [];
$issue = Repo::issue()->get($issueId);
$issueDoiId = $issue->getData('doiId');
/** @var JournalDAO $contextDao */
$contextDao = DAORegistry::getDAO('JournalDAO');
/** @var Journal $context */
$context = $contextDao->getById($issue->getData('journalId'));
if (!empty($issueDoiId)) {
if ($enabledDoiTypesOnly == false || ($enabledDoiTypesOnly && $context->isDoiTypeEnabled(self::TYPE_ISSUE))) {
$doiIds[] = $issueDoiId;
}
}
return $doiIds;
}
/**
* Schedules DOI deposits with the active registration agency for all valid and
* unregistered/stale publication items. Items are added as a queued job to be
* completed asynchronously.
*
*
*/
public function depositAll(Context $context)
{
parent::depositAll($context);
if (in_array(Repo::doi()::TYPE_ISSUE, $context->getData(Context::SETTING_ENABLED_DOI_TYPES) ?? [])) {
// If there is no configured registration agency, nothing can be deposited.
$agency = $context->getConfiguredDoiAgency();
if (!$agency) {
return;
}
/** @var DAO */
$dao = $this->dao;
$issuesCollection = $dao->getAllDepositableIssueIds($context);
$issueData = $issuesCollection->reduce(function ($carry, $item) {
$carry['issueIds'][] = $item->issue_id;
$carry['doiIds'][] = $item->doi_id;
return $carry;
}, ['issueIds' => [], 'doiIds' => []]);
// Schedule/queue jobs for issues
foreach ($issueData['issueIds'] as $issueId) {
dispatch(new DepositIssue($issueId, $context, $agency));
}
// Mark issue DOIs as submitted
Repo::doi()->markSubmitted($issueData['doiIds']);
}
}
/**
* Checks whether a DOI object is referenced by ID on any pub objects for a given pub object type.
*
* @param string $pubObjectType One of Repo::doi()::TYPE_* constants
*/
public function isAssigned(int $doiId, string $pubObjectType): bool
{
$isAssigned = match ($pubObjectType) {
Repo::doi()::TYPE_REPRESENTATION => Repo::galley()
->getCollector()
->filterByDoiIds([$doiId])
->getIds()
->count(),
default => false,
};
return $isAssigned || parent::isAssigned($doiId, $pubObjectType);
}
/**
* Get app-specific DOI type constants to check when scheduling deposit for submissions
*/
protected function getValidSubmissionDoiTypes(): array
{
return [
self::TYPE_PUBLICATION,
self::TYPE_REPRESENTATION,
];
}
/**
* Gets legacy, user-generated suffix pattern associated with object type and context
*
*
* @return mixed|null
*/
private function getPubIdSuffixPattern(DataObject $object, Context $context)
{
if ($object instanceof Issue) {
return $context->getData(Repo::doi()::CUSTOM_ISSUE_PATTERN);
} elseif ($object instanceof Representation) {
return $context->getData(Repo::doi()::CUSTOM_REPRESENTATION_PATTERN);
} else {
return $context->getData(Repo::doi()::CUSTOM_PUBLICATION_PATTERN);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* @file classes/emailTemplate/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @brief Read and write email templates to the database.
*/
namespace APP\emailTemplate;
class DAO extends \PKP\emailTemplate\DAO
{
/**
* Renames app-specific email template variables during installation
*/
protected function variablesToRename(): array
{
return [
'contextName' => 'journalName',
'contextUrl' => 'journalUrl',
'contextSignature' => 'journalSignature',
];
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
/**
* @file classes/facades/Repo.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repo
*
* @brief Extends the base Repo facade with any overrides for OJS
*/
namespace APP\facades;
use APP\decision\Repository as DecisionRepository;
use APP\doi\Repository as DoiRepository;
use APP\issue\Repository as IssueRepository;
use APP\mail\Repository as MailRepository;
use APP\publication\Repository as PublicationRepository;
use APP\section\Repository as SectionRepository;
use APP\submission\Repository as SubmissionRepository;
use APP\submissionFile\Repository as SubmissionFileRepository;
use APP\user\Repository as UserRepository;
use PKP\facades\Repo as BaseRepo;
use PKP\galley\Repository as GalleyRepository;
use PKP\highlight\Repository as HighlightRepository;
class Repo extends BaseRepo
{
public static function doi(): DoiRepository
{
return app(DoiRepository::class);
}
public static function decision(): DecisionRepository
{
return app(DecisionRepository::class);
}
public static function galley(): GalleyRepository
{
return app(GalleyRepository::class);
}
public static function highlight(): HighlightRepository
{
return app(HighlightRepository::class);
}
public static function issue(): IssueRepository
{
return app(IssueRepository::class);
}
public static function publication(): PublicationRepository
{
return app(PublicationRepository::class);
}
public static function section(): SectionRepository
{
return app(SectionRepository::class);
}
public static function submission(): SubmissionRepository
{
return app(SubmissionRepository::class);
}
public static function submissionFile(): SubmissionFileRepository
{
return app(SubmissionFileRepository::class);
}
public static function user(): UserRepository
{
return app(UserRepository::class);
}
public static function mailable(): MailRepository
{
return app(MailRepository::class);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
/**
* @file classes/file/IssueFileManager.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueFileManager
*
* @ingroup file
*
* @brief Class defining operations for issue file management.
*
* Issue directory structure:
* [issue id]/public
*/
namespace APP\file;
use APP\facades\Repo;
use APP\issue\IssueFile;
use APP\issue\IssueFileDAO;
use PKP\config\Config;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use PKP\file\TemporaryFile;
use PKP\plugins\Hook;
class IssueFileManager extends FileManager
{
/** @var string the path to location of the files */
public $_filesDir = null;
/** @var int the associated issue ID */
public $_issueId = null;
/**
* Constructor.
* Create a manager for handling issue files.
*
* @param int $issueId
*/
public function __construct($issueId)
{
$issue = Repo::issue()->get($issueId);
assert(isset($issue));
$this->setIssueId($issueId);
$this->setFilesDir(Config::getVar('files', 'files_dir') . '/journals/' . $issue->getJournalId() . '/issues/' . $issueId . '/');
parent::__construct();
}
/**
* Get the issue files directory.
*
* @return string
*/
public function getFilesDir()
{
return $this->_filesDir;
}
/**
* Set the issue files directory.
*
* @param string $filesDir
*/
public function setFilesDir($filesDir)
{
$this->_filesDir = $filesDir;
}
/**
* Get the issue ID.
*
* @return int
*/
public function getIssueId()
{
return $this->_issueId;
}
/**
* Set the issue ID.
*
* @param int $issueId
*/
public function setIssueId($issueId)
{
$this->_issueId = (int) $issueId;
}
/**
* Delete an issue file by ID.
*
* @param int $fileId
*
* @return bool if successful
*/
public function deleteById($fileId)
{
$issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
$issueFile = $issueFileDao->getById($fileId);
if (parent::deleteByPath($this->getFilesDir() . $this->contentTypeToPath($issueFile->getContentType()) . '/' . $issueFile->getServerFileName())) {
$issueFileDao->deleteById($fileId);
return true;
}
return false;
}
/**
* Delete the entire tree of files belonging to an issue.
*/
public function deleteIssueTree()
{
parent::rmtree($this->getFilesDir());
}
/**
* Download a file.
*
* @param int $fileId the file id of the file to download
* @param bool $inline print file as inline instead of attachment, optional
*
* @return bool
*/
public function downloadById($fileId, $inline = false)
{
$issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
$issueFile = $issueFileDao->getById($fileId);
if ($issueFile) {
$fileType = $issueFile->getFileType();
$filePath = $this->getFilesDir() . $this->contentTypeToPath($issueFile->getContentType()) . '/' . $issueFile->getServerFileName();
return parent::downloadByPath($filePath, $fileType, $inline, $issueFile->getOriginalFileName());
} else {
return false;
}
}
/**
* Return directory path based on issue content type (used for naming files).
*
* @param int $contentType
*
* @return string
*/
public function contentTypeToPath($contentType)
{
switch ($contentType) {
case IssueFile::ISSUE_FILE_PUBLIC: return 'public';
}
}
/**
* Return abbreviation based on issue content type (used for naming files).
*
* @param int $contentType
*
* @return string
*/
public function contentTypeToAbbrev($contentType)
{
switch ($contentType) {
case IssueFile::ISSUE_FILE_PUBLIC: return 'PB';
}
}
/**
* Create an issue galley based on a temporary file.
*
* @param TemporaryFile $temporaryFile
* @param int $contentType Issue file content type
*
* @return ?IssueFile|false the resulting issue file
*/
public function fromTemporaryFile($temporaryFile, $contentType = IssueFile::ISSUE_FILE_PUBLIC)
{
$result = null;
if (Hook::call('IssueFileManager::fromTemporaryFile', [&$temporaryFile, &$contentType, &$result])) {
return $result;
}
$issueId = $this->getIssueId();
$issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
$contentTypePath = $this->contentTypeToPath($contentType);
$dir = $this->getFilesDir() . $contentTypePath . '/';
$issueFile = $issueFileDao->newDataObject();
$issueFile->setIssueId($issueId);
$issueFile->setDateUploaded($temporaryFile->getDateUploaded());
$issueFile->setDateModified(Core::getCurrentDate());
$issueFile->setServerFileName(''); // Blank until we insert to generate a file ID
$issueFile->setFileType($temporaryFile->getFileType());
$issueFile->setFileSize($temporaryFile->getFileSize());
$issueFile->setOriginalFileName($temporaryFile->getOriginalFileName());
$issueFile->setContentType($contentType);
if (!$issueFileDao->insertObject($issueFile)) {
return false;
}
$extension = $this->parseFileExtension($issueFile->getOriginalFileName());
$newFileName = $issueFile->getIssueId() . '-' . $issueFile->getId() . '-' . $this->contentTypeToAbbrev($contentType) . '.' . $extension;
$issueFile->setServerFileName($newFileName);
// Copy the actual file
if (!$this->copyFile($temporaryFile->getFilePath(), $dir . $newFileName)) {
// Upload failed; remove the new DB record.
$issueFileDao->deleteById($issueFile->getId());
return false;
}
// Upload succeeded. Update issue file record with new filename.
$issueFileDao->updateObject($issueFile);
return $issueFile;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\file\IssueFileManager', '\IssueFileManager');
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* @file classes/file/LibraryFileManager.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class LibraryFileManager
*
* @ingroup file
*
* @brief Wrapper class for uploading files to a site/context' library directory.
*/
namespace APP\file;
use PKP\file\PKPLibraryFileManager;
class LibraryFileManager extends PKPLibraryFileManager
{
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\file\LibraryFileManager', '\LibraryFileManager');
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* @file classes/file/PublicFileManager.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PublicFileManager
*
* @ingroup file
*
* @brief Wrapper class for uploading files to a site/journal's public directory.
*/
namespace APP\file;
use PKP\config\Config;
use PKP\file\PKPPublicFileManager;
class PublicFileManager extends PKPPublicFileManager
{
/**
* @copydoc PKPPublicFileManager::getContextFilesPath()
*/
public function getContextFilesPath($contextId)
{
return Config::getVar('files', 'public_files_dir') . '/journals/' . (int) $contextId;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\file\PublicFileManager', '\PublicFileManager');
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* @file classes/handler/Handler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Handler
*
* @ingroup handler
*
* @brief Base request handler application class
*/
namespace APP\handler;
use PKP\handler\PKPHandler;
class Handler extends PKPHandler
{
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\handler\Handler', '\Handler');
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* @defgroup i18n I18N
* Implements localization concerns such as locale files, time zones, and country lists.
*/
/**
* @file classes/i18n/AppLocale.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPLocale
*
* @ingroup i18n
*
* @brief Deprecated class, kept only for backwards compatibility with external plugins
*/
namespace APP\i18n;
use PKP\i18n\PKPLocale;
if (!PKP_STRICT_MODE) {
/**
* @deprecated The class \APP\i18n\AppLocale has been replaced by PKP\facades\Locale
*/
class AppLocale extends PKPLocale
{
}
class_alias('\APP\i18n\AppLocale', '\AppLocale');
}
+62
View File
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/install/Install.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Install
*
* @ingroup install
*
* @see Installer, InstallForm
*
* @brief Perform system installation.
*
* This script will:
* - Create the database (optionally), and install the database tables and initial data.
* - Update the config file with installation parameters.
*/
namespace APP\install;
use PKP\install\PKPInstall;
class Install extends PKPInstall
{
/**
* Constructor.
*
* @see install.form.InstallForm for the expected parameters
*
* @param array $params installer parameters
* @param string $descriptor descriptor path
* @param bool $isPlugin true iff a plugin is being installed
*/
public function __construct($params, $descriptor = 'install.xml', $isPlugin = false)
{
parent::__construct($descriptor, $params, $isPlugin);
}
//
// Installer actions
//
/**
* Get the names of the directories to create.
*
* @return array
*/
public function getCreateDirectories()
{
$directories = parent::getCreateDirectories();
$directories[] = 'journals';
return $directories;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\install\Install', '\Install');
}
+691
View File
@@ -0,0 +1,691 @@
<?php
/**
* @file classes/install/Upgrade.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Upgrade
*
* @ingroup install
*
* @brief Perform system upgrade.
*/
namespace APP\install;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use APP\journal\JournalDAO;
use APP\template\TemplateManager;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use PKP\identity\Identity;
use PKP\install\Installer;
use PKP\navigationMenu\NavigationMenuItemDAO;
use PKP\plugins\PluginSettingsDAO;
use PKP\security\Role;
use PKP\site\SiteDAO;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submissionFile\SubmissionFile;
class Upgrade extends Installer
{
protected $appEmailTemplateVariableNames = [
'contextName' => 'journalName',
'contextUrl' => 'journalUrl',
'contextSignature' => 'journalSignature',
];
/**
* Constructor.
*
* @param array $params upgrade parameters
* @param string $installFile Name of XML descriptor to install
* @param bool $isPlugin True iff the installer is for a plugin.
*/
public function __construct($params, $installFile = 'upgrade.xml', $isPlugin = false)
{
parent::__construct($installFile, $params, $isPlugin);
}
/**
* Returns true iff this is an upgrade process.
*
* @return bool
*/
public function isUpgrade()
{
return true;
}
//
// Upgrade actions
//
/**
* Rebuild the search index.
*
* @return bool
*/
public function rebuildSearchIndex()
{
$submissionSearchIndex = Application::getSubmissionSearchIndex();
$submissionSearchIndex->rebuildIndex();
return true;
}
/**
* Clear the CSS cache files (needed when changing LESS files)
*
* @return bool
*/
public function clearCssCache()
{
$request = Application::get()->getRequest();
$templateMgr = TemplateManager::getManager($request);
$templateMgr->clearCssCache();
return true;
}
/**
* For 3.1.0 upgrade (#2467): In multi-journal upgrades from OJS 2.x, the
* user_group_id column in the authors table may be updated to point to
* user groups in other journals.
*
* @return bool
*/
public function fixAuthorGroup()
{
$rows = DB::table('authors as a')
->leftJoin('submissions as s', 's.submission_id', '=', 'a.submission_id')
->leftJoin('user_groups as g', 'a.user_group_id', '=', 'g.user_group_id')
->whereColumn('g.context_id', '<>', 's.context_id')
->get(['a.author_id', 's.context_id']);
foreach ($rows as $row) {
$authorGroup = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_AUTHOR], $row->context_id, true);
if ($authorGroup) {
DB::table('authors')
->where('author_id', '=', $row->author_id)
->update(['user_group_id' => $authorGroup->getId()]);
}
}
return true;
}
/**
* For 3.0.x - 3.1.1 upgrade: repair the migration of the supp files.
*
* @return bool True indicates success.
*/
public function repairSuppFilesFilestage()
{
$fileManager = new FileManager();
$rows = DB::table('submission_supplementary_files as ssf')
->leftJoin('submission_files as sf', 'sf.file_id', '=', 'ssf.file_id')
->leftJoin('submissions as s', 's.submission_id', '=', 'sf.submission_id')
->where('sf.file_stage', '=', SubmissionFile::SUBMISSION_FILE_SUBMISSION)
->where('sf.assoc_type', '=', Application::ASSOC_TYPE_REPRESENTATION)
->whereColumn('sf.revision', '=', 'ssf.revision')
->get();
foreach ($rows as $row) {
$submissionDir = Repo::submissionFile()
->getSubmissionDir($row->context_id, $row->submission_id);
$generatedOldFilename = sprintf(
'%d-%s-%d-%d-%d-%s.%s',
$row->submission_id,
$row->genre_id,
$row->file_id,
$row->revision,
$row->file_stage,
date('Ymd', strtotime($row->date_uploaded)),
strtolower_codesafe($fileManager->parseFileExtension($row->original_file_name))
);
$generatedNewFilename = sprintf(
'%d-%s-%d-%d-%d-%s.%s',
$row->submission_id,
$row->genre_id,
$row->file_id,
$row->revision,
SubmissionFile::SUBMISSION_FILE_PROOF,
date('Ymd', strtotime($row->date_uploaded)),
strtolower_codesafe($fileManager->parseFileExtension($row->original_file_name))
);
$oldFileName = $submissionDir . '/' . $this->_fileStageToPath($row->file_stage) . '/' . $generatedOldFilename;
$newFileName = $submissionDir . '/' . $this->_fileStageToPath($row->file_stage) . '/' . $generatedNewFilename;
if (!Services::get('file')->fs->rename($oldFileName, $newFileName)) {
error_log("Unable to move \"{$oldFileName}\" to \"{$newFileName}\".");
}
DB::table('submission_files')
->where('file_id', '=', $row->file_id)
->where('revision', '=', $row->revision)
->update(['file_stage' => SubmissionFile::SUBMISSION_FILE_PROOF]);
}
return true;
}
/**
* If StaticPages table exists we should port the data as NMIs
*
* @return bool
*/
public function migrateStaticPagesToNavigationMenuItems()
{
if ($this->tableExists('static_pages')) {
$contextDao = Application::getContextDAO();
$navigationMenuItemDao = DAORegistry::getDAO('NavigationMenuItemDAO'); /** @var NavigationMenuItemDAO $navigationMenuItemDao */
$staticPagesDao = new \APP\plugins\generic\staticPages\classes\StaticPagesDAO();
$contexts = $contextDao->getAll();
while ($context = $contexts->next()) {
$contextStaticPages = $staticPagesDao->getByContextId($context->getId())->toAssociativeArray();
foreach ($contextStaticPages as $staticPage) {
$retNMIId = $navigationMenuItemDao->portStaticPage($staticPage);
if ($retNMIId) {
$staticPagesDao->deleteById($staticPage->getId());
} else {
error_log('WARNING: The StaticPage "' . $staticPage->getLocalizedTitle() . '" uses a path (' . $staticPage->getPath() . ') that conflicts with an existing Custom Navigation Menu Item path. Skipping this StaticPage.');
}
}
}
}
return true;
}
/**
* Migrate sr_SR locale to the new sr_RS@latin.
*
* @return bool
*/
public function migrateSRLocale()
{
$oldLocale = 'sr_SR';
$newLocale = 'sr_RS@latin';
$oldLocaleStringLength = 's:5';
$journalSettingsDao = new class () extends \PKP\db\DAO {
/**
* Method for update journal setting
*
* @param int $journalId
* @param string $name
* @param string $type data type of the setting. If omitted, type will be guessed
* @param bool $isLocalized
*/
public function updateSetting($journalId, $name, $value, $type = null, $isLocalized = false)
{
if (!$isLocalized) {
$value = $this->convertToDB($value, $type);
DB::table('journal_settings')->updateOrInsert(
['journal_id' => (int) $journalId, 'setting_name' => $name, 'locale' => ''],
['setting_value' => $value, 'setting_type' => $type]
);
} else {
if (is_array($value)) {
foreach ($value as $locale => $localeValue) {
$this->update('DELETE FROM journal_settings WHERE journal_id = ? AND setting_name = ? AND locale = ?', [(int) $journalId, $name, $locale]);
if (empty($localeValue)) {
continue;
}
$type = null;
$this->update(
'INSERT INTO journal_settings (journal_id, setting_name, setting_value, setting_type, locale) VALUES (?, ?, ?, ?, ?)',
[$journalId, $name, $this->convertToDB($localeValue, $type), $type, $locale]
);
}
}
}
}
/**
* Retrieve a context setting value.
*
* @param string $name
* @param string $locale optional
*/
public function getSetting($journalId, $name, $locale = null)
{
$params = [(int) $journalId, $name];
if ($locale) {
$params[] = $locale;
}
$result = $this->retrieve(
'SELECT setting_name, setting_value, setting_type, locale
FROM journal_settings
WHERE journal_id = ? AND
setting_name = ?' .
($locale ? ' AND locale = ?' : ''),
$params
);
$returner = [];
foreach ($result as $row) {
$returner[$row->locale] = $this->convertFromDB($row->setting_value, $row->setting_type);
}
if (count($returner) == 1) {
return array_shift($returner);
}
if (count($returner) == 0) {
return false;
}
return $returner;
}
};
// Check if the sr_SR is used, and if not do not run further
$srExistResult = $journalSettingsDao->retrieve('SELECT COUNT(*) AS row_count FROM site WHERE installed_locales LIKE ?', ['%' . $oldLocale . '%']);
$row = $srExistResult->current();
$srExist = $row && $row->row_count;
if (!$srExist) {
return true;
}
// Consider all DB tables that have locale column:
$dbTables = [
'announcement_settings', 'announcement_type_settings', 'author_settings', 'books_for_review_settings', 'citation_settings', 'controlled_vocab_entry_settings',
'data_object_tombstone_settings', 'email_templates_data', 'email_templates_default_data', 'external_feed_settings', 'filter_settings', 'genre_settings', 'group_settings',
'issue_galleys', 'issue_galley_settings', 'issue_settings', 'journal_settings', 'library_file_settings',
'navigation_menu_item_assignment_settings', 'navigation_menu_item_settings', 'notification_settings', 'referral_settings',
'review_form_element_settings', 'review_form_settings', 'review_object_metadata_settings', 'review_object_type_settings', 'section_settings', 'site_settings',
'static_page_settings', 'submissions', 'submission_file_settings', 'submission_galleys', 'submission_galley_settings', 'submission_settings', 'subscription_type_settings',
'user_group_settings', 'user_settings',
];
foreach ($dbTables as $dbTable) {
if ($this->tableExists($dbTable)) {
$journalSettingsDao->update('UPDATE ' . $dbTable . ' SET locale = ? WHERE locale = ?', [$newLocale, $oldLocale]);
}
}
// Consider other locale columns
$journalSettingsDao->update('UPDATE journals SET primary_locale = ? WHERE primary_locale = ?', [$newLocale, $oldLocale]);
$journalSettingsDao->update('UPDATE site SET primary_locale = ? WHERE primary_locale = ?', [$newLocale, $oldLocale]);
$journalSettingsDao->update('UPDATE site SET installed_locales = REPLACE(installed_locales, ?, ?)', [$oldLocale, $newLocale]);
$journalSettingsDao->update('UPDATE site SET supported_locales = REPLACE(supported_locales, ?, ?)', [$oldLocale, $newLocale]);
$journalSettingsDao->update('UPDATE users SET locales = REPLACE(locales, ?, ?)', [$oldLocale, $newLocale]);
// journal_settings
// Consider array setting values from the setting names:
// supportedFormLocales, supportedLocales, supportedSubmissionLocales
$settingNames = "('supportedFormLocales', 'supportedLocales', 'supportedSubmissionLocales')";
// As a precaution use $oldLocaleStringLength, to exclude that the text contain the old locale string
$settingValueResult = $journalSettingsDao->retrieve('SELECT * FROM journal_settings WHERE setting_name IN ' . $settingNames . ' AND setting_value LIKE ? AND setting_type = \'object\'', ['%' . $oldLocaleStringLength . ':"' . $oldLocale . '%']);
foreach ($settingValueResult as $row) {
$arraySettingValue = $journalSettingsDao->getSetting($row->journal_id, $row->setting_name);
for ($i = 0; $i < count($arraySettingValue); $i++) {
if ($arraySettingValue[$i] == $oldLocale) {
$arraySettingValue[$i] = $newLocale;
}
}
$journalSettingsDao->updateSetting($row->journal_id, $row->setting_name, $arraySettingValue);
}
// Consider journal images
// Note that the locale column values are already changed above
$publicFileManager = new PublicFileManager();
$settingNames = "('homeHeaderLogoImage', 'homeHeaderTitleImage', 'homepageImage', 'journalFavicon', 'journalThumbnail', 'pageHeaderLogoImage', 'pageHeaderTitleImage')";
$settingValueResult = $journalSettingsDao->retrieve('SELECT * FROM journal_settings WHERE setting_name IN ' . $settingNames . ' AND locale = ? AND setting_value LIKE ? AND setting_type = \'object\'', [$newLocale, '%' . $oldLocale . '%']);
foreach ($settingValueResult as $row) {
$arraySettingValue = $journalSettingsDao->getSetting($row->journal_id, $row->setting_name, $newLocale);
$oldUploadName = $arraySettingValue['uploadName'];
$newUploadName = str_replace('_' . $oldLocale . '.', '_' . $newLocale . '.', $oldUploadName);
if ($publicFileManager->fileExists($publicFileManager->getContextFilesPath($row->journal_id) . '/' . $oldUploadName)) {
$publicFileManager->copyContextFile($row->journal_id, $publicFileManager->getContextFilesPath($row->journal_id) . '/' . $oldUploadName, $newUploadName);
$publicFileManager->removeContextFile($row->journal_id, $oldUploadName);
}
$arraySettingValue['uploadName'] = $newUploadName;
$newArraySettingValue[$newLocale] = $arraySettingValue;
$journalSettingsDao->updateSetting($row->journal_id, $row->setting_name, $newArraySettingValue, 'object', true);
}
// Consider issue cover images
// Note that the locale column values are already changed above
$settingValueResult = $journalSettingsDao->retrieve('SELECT a.*, b.journal_id FROM issue_settings a, issues b WHERE a.setting_name = \'fileName\' AND a.locale = ? AND a.setting_value LIKE ? AND a.setting_type = \'string\' AND b.issue_id = a.issue_id', [$newLocale, '%' . $oldLocale . '%']);
foreach ($settingValueResult as $row) {
$oldCoverImage = $row->setting_value;
$newCoverImage = str_replace('_' . $oldLocale . '.', '_' . $newLocale . '.', $oldCoverImage);
if ($publicFileManager->fileExists($publicFileManager->getContextFilesPath($row->journal_id) . '/' . $oldCoverImage)) {
$publicFileManager->copyContextFile($row->journal_id, $publicFileManager->getContextFilesPath($row->journal_id) . '/' . $oldCoverImage, $newCoverImage);
$publicFileManager->removeContextFile($row->journal_id, $oldCoverImage);
}
$journalSettingsDao->update('UPDATE issue_settings SET setting_value = ? WHERE issue_id = ? AND setting_name = \'fileName\' AND locale = ?', [$newCoverImage, (int) $row->issue_id, $newLocale]);
}
// Consider article cover images
// Note that the locale column values are already changed above
$settingValueResult = $journalSettingsDao->retrieve('SELECT a.*, b.context_id FROM submission_settings a, submissions b WHERE a.setting_name = \'fileName\' AND a.locale = ? AND a.setting_value LIKE ? AND b.submission_id = a.submission_id', [$newLocale, '%' . $oldLocale . '%']);
foreach ($settingValueResult as $row) {
$oldCoverImage = $row->setting_value;
$newCoverImage = str_replace('_' . $oldLocale . '.', '_' . $newLocale . '.', $oldCoverImage);
if ($publicFileManager->fileExists($publicFileManager->getContextFilesPath($row->context_id) . '/' . $oldCoverImage)) {
$publicFileManager->copyContextFile($row->context_id, $publicFileManager->getContextFilesPath($row->context_id) . '/' . $oldCoverImage, $newCoverImage);
$publicFileManager->removeContextFile($row->context_id, $oldCoverImage);
}
$journalSettingsDao->update('UPDATE submission_settings SET setting_value = ? WHERE submission_id = ? AND setting_name = \'fileName\' AND locale = ?', [$newCoverImage, (int) $row->submission_id, $newLocale]);
}
// plugin_settings
// Consider array setting values from the setting names:
// blockContent (from a custom block plugin), additionalInformation (from objects for review plugin)
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */
$settingNames = "('blockContent', 'additionalInformation')";
$settingValueResult = $pluginSettingsDao->retrieve('SELECT * FROM plugin_settings WHERE setting_name IN ' . $settingNames . ' AND setting_value LIKE ?', ['%' . $oldLocaleStringLength . ':"' . $oldLocale . '%']);
foreach ($settingValueResult as $row) {
$arraySettingValue = $pluginSettingsDao->getSetting($row->context_id, $row->plugin_name, $row->setting_name);
$arraySettingValue[$newLocale] = $arraySettingValue[$oldLocale];
unset($arraySettingValue[$oldLocale]);
$pluginSettingsDao->updateSetting($row->context_id, $row->plugin_name, $row->setting_name, $arraySettingValue);
}
return true;
}
/**
* Migrate first and last user names as multilingual into the DB table user_settings.
*
* @return bool
*/
public function migrateUserAndAuthorNames()
{
// the user names will be saved in the site's primary locale
DB::insert("INSERT INTO user_settings (user_id, locale, setting_name, setting_value, setting_type) SELECT DISTINCT u.user_id, s.primary_locale, ?, u.first_name, 'string' FROM users_tmp u, site s", [Identity::IDENTITY_SETTING_GIVENNAME]);
DB::insert("INSERT INTO user_settings (user_id, locale, setting_name, setting_value, setting_type) SELECT DISTINCT u.user_id, s.primary_locale, ?, u.last_name, 'string' FROM users_tmp u, site s", [Identity::IDENTITY_SETTING_FAMILYNAME]);
// the author names will be saved in the submission's primary locale
DB::insert("INSERT INTO author_settings (author_id, locale, setting_name, setting_value, setting_type) SELECT DISTINCT a.author_id, s.locale, ?, a.first_name, 'string' FROM authors_tmp a, submissions s WHERE s.submission_id = a.submission_id", [Identity::IDENTITY_SETTING_GIVENNAME]);
DB::insert("INSERT INTO author_settings (author_id, locale, setting_name, setting_value, setting_type) SELECT DISTINCT a.author_id, s.locale, ?, a.last_name, 'string' FROM authors_tmp a, submissions s WHERE s.submission_id = a.submission_id", [Identity::IDENTITY_SETTING_FAMILYNAME]);
// middle name will be migrated to the given name
// note that given names are already migrated to the settings table
switch (Config::getVar('database', 'driver')) {
case 'mysql':
case 'mysqli':
// the alias for _settings table cannot be used for some reason -- syntax error
DB::update("UPDATE user_settings, users_tmp u SET user_settings.setting_value = CONCAT(user_settings.setting_value, ' ', u.middle_name) WHERE user_settings.setting_name = ? AND u.user_id = user_settings.user_id AND u.middle_name IS NOT NULL AND u.middle_name <> ''", [Identity::IDENTITY_SETTING_GIVENNAME]);
DB::update("UPDATE author_settings, authors_tmp a SET author_settings.setting_value = CONCAT(author_settings.setting_value, ' ', a.middle_name) WHERE author_settings.setting_name = ? AND a.author_id = author_settings.author_id AND a.middle_name IS NOT NULL AND a.middle_name <> ''", [Identity::IDENTITY_SETTING_GIVENNAME]);
break;
case 'postgres':
case 'postgres64':
case 'postgres7':
case 'postgres8':
case 'postgres9':
DB::update("UPDATE user_settings SET setting_value = CONCAT(setting_value, ' ', u.middle_name) FROM users_tmp u WHERE user_settings.setting_name = ? AND u.user_id = user_settings.user_id AND u.middle_name IS NOT NULL AND u.middle_name <> ''", [Identity::IDENTITY_SETTING_GIVENNAME]);
DB::update("UPDATE author_settings SET setting_value = CONCAT(setting_value, ' ', a.middle_name) FROM authors_tmp a WHERE author_settings.setting_name = ? AND a.author_id = author_settings.author_id AND a.middle_name IS NOT NULL AND a.middle_name <> ''", [Identity::IDENTITY_SETTING_GIVENNAME]);
break;
default: fatalError('Unknown database type!');
}
// salutation and suffix will be migrated to the preferred public name
// user preferred public names will be inserted for each supported site locales
$siteDao = DAORegistry::getDAO('SiteDAO'); /** @var SiteDAO $siteDao */
$site = $siteDao->getSite();
$supportedLocales = $site->getSupportedLocales();
$userResult = DB::select(
"SELECT user_id, first_name, last_name, middle_name, salutation, suffix FROM users_tmp
WHERE (salutation IS NOT NULL AND salutation <> '') OR
(suffix IS NOT NULL AND suffix <> '')"
);
foreach ($userResult as $row) {
$userId = $row->user_id;
$firstName = $row->first_name;
$lastName = $row->last_name;
$middleName = $row->middle_name;
$salutation = $row->salutation;
$suffix = $row->suffix;
foreach ($supportedLocales as $siteLocale) {
$preferredPublicName = ($salutation != '' ? "{$salutation} " : '') . "{$firstName} " . ($middleName != '' ? "{$middleName} " : '') . $lastName . ($suffix != '' ? ", {$suffix}" : '');
DB::insert(
"INSERT INTO user_settings (user_id, locale, setting_name, setting_value, setting_type) VALUES (?, ?, 'preferredPublicName', ?, 'string')",
[(int) $userId, $siteLocale, $preferredPublicName]
);
}
}
// author suffix will be migrated to the author preferred public name
// author preferred public names will be inserted for each journal supported locale
// get supported locales for all journals
$journalDao = DAORegistry::getDAO('JournalDAO'); /** @var JournalDAO $journalDao */
$journals = $journalDao->getAll();
$journalsSupportedLocales = [];
while ($journal = $journals->next()) {
$journalsSupportedLocales[$journal->getId()] = $journal->getSupportedLocales();
}
// get all authors with a suffix
$authorResult = DB::select(
"SELECT a.author_id, a.first_name, a.last_name, a.middle_name, a.suffix, j.journal_id FROM authors_tmp a
LEFT JOIN submissions s ON (s.submission_id = a.submission_id)
LEFT JOIN journals j ON (j.journal_id = s.context_id)
WHERE suffix IS NOT NULL AND suffix <> ''"
);
foreach ($authorResult as $row) {
$authorId = $row->author_id;
$firstName = $row->first_name;
$lastName = $row->last_name;
$middleName = $row->middle_name;
$suffix = $row->suffix;
$journalId = $row->journal_id;
$supportedLocales = $journalsSupportedLocales[$journalId];
foreach ($supportedLocales as $locale) {
$preferredPublicName = "{$firstName} " . ($middleName != '' ? "{$middleName} " : '') . $lastName . ($suffix != '' ? ", {$suffix}" : '');
DB::insert(
"INSERT INTO author_settings (author_id, locale, setting_name, setting_value, setting_type) VALUES (?, ?, 'preferredPublicName', ?, 'string')",
[(int) $authorId, $locale, $preferredPublicName]
);
}
}
// remove temporary table
$siteDao->update('DROP TABLE users_tmp');
$siteDao->update('DROP TABLE authors_tmp');
return true;
}
/**
* Update assoc_id for assoc_type Application::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 531
*
* @return bool True indicates success.
*/
public function updateSuppFileMetrics()
{
// Copy 531 assoc_type data to temp table
DB::statement('CREATE TABLE metrics_supp AS (SELECT * FROM metrics WHERE assoc_type = 531)');
// Fetch submission_file data with old-supp-id
$result = DB::select(
'SELECT * FROM submission_file_settings WHERE setting_name = ?',
['old-supp-id']
);
// Loop through the data and save to temp table
foreach ($result as $row) {
// Use assoc_type 2531 to prevent collisions between old assoc_id and new assoc_id
DB::update('UPDATE metrics_supp SET assoc_id = ?, assoc_type = ? WHERE assoc_type = ? AND assoc_id = ?', [(int) $row->file_id, 2531, 531, (int) $row->setting_value]);
}
// update temprorary 2531 values to 531 values
DB::update('UPDATE metrics_supp SET assoc_type = ? WHERE assoc_type = ?', [531, 2531]);
// delete all existing 531 values from the actual metrics table
DB::statement('DELETE FROM metrics WHERE assoc_type = 531');
// copy updated 531 values from metrics_supp to metrics table
DB::insert('INSERT INTO metrics SELECT * FROM metrics_supp');
// Drop metrics_supp table
DB::statement('DROP TABLE metrics_supp');
return true;
}
/**
* Add an entry for the site stylesheet to the site_settings database when it
* exists
*/
public function migrateSiteStylesheet()
{
$siteDao = DAORegistry::getDAO('SiteDAO'); /** @var SiteDAO $siteDao */
$publicFileManager = new PublicFileManager();
if (!file_exists($publicFileManager->getSiteFilesPath() . '/sitestyle.css')) {
return true;
}
$site = $siteDao->getSite();
$site->setData('styleSheet', 'sitestyle.css');
$siteDao->updateObject($site);
return true;
}
/**
* Copy a context's copyrightNotice to a new licenseTerms setting, leaving
* the copyrightNotice in place.
*/
public function createLicenseTerms()
{
$contextDao = Application::getContextDao();
$result = $contextDao->retrieve('SELECT * from ' . $contextDao->settingsTableName . " WHERE setting_name='copyrightNotice'");
foreach ($result as $row) {
$row = (array) $row;
$contextDao->update(
'
INSERT INTO ' . $contextDao->settingsTableName . ' (
' . $contextDao->primaryKeyColumn . ',
locale,
setting_name,
setting_value
) VALUES (?, ?, ?, ?)',
[
$row[$contextDao->primaryKeyColumn],
$row['locale'],
'licenseTerms',
$row['setting_value'],
]
);
}
return true;
}
/**
* Update permit_metadata_edit and can_change_metadata for user_groups and stage_assignments tables.
*
* @return bool True indicates success.
*/
public function changeUserRolesAndStageAssignmentsForStagePermitSubmissionEdit()
{
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$roles = Repo::userGroup()::NOT_CHANGE_METADATA_EDIT_PERMISSION_ROLES;
$roleString = '(' . implode(',', $roles) . ')';
DB::table('user_groups')
->whereIn('role_id', $roles)
->update(['permit_metadata_edit' => 1]);
switch (Config::getVar('database', 'driver')) {
case 'mysql':
case 'mysqli':
$stageAssignmentDao->update('UPDATE stage_assignments sa JOIN user_groups ug on sa.user_group_id = ug.user_group_id SET sa.can_change_metadata = 1 WHERE ug.role_id IN ' . $roleString);
break;
case 'postgres':
case 'postgres64':
case 'postgres7':
case 'postgres8':
case 'postgres9':
$stageAssignmentDao->update('UPDATE stage_assignments sa SET can_change_metadata=1 FROM user_groups ug WHERE sa.user_group_id = ug.user_group_id AND ug.role_id IN ' . $roleString);
break;
default: fatalError('Unknown database type!');
}
return true;
}
/**
* Update how submission cover images are stored
*
* Combines the coverImage and coverImageAltText settings in the
* submissions table into an assoc array stored under the coverImage
* setting.
*
* This will be migrated to the publication_settings table in
* 3.2.0_versioning.xml.
*/
public function migrateSubmissionCoverImages()
{
$coverImagesBySubmission = [];
$deprecatedDao = Repo::submission()->dao->deprecatedDao;
$result = $deprecatedDao->retrieve(
'SELECT * from submission_settings WHERE setting_name=\'coverImage\' OR setting_name=\'coverImageAltText\''
);
foreach ($result as $row) {
$submissionId = $row->submission_id;
if (empty($coverImagesBySubmission[$submissionId])) {
$coverImagesBySubmission[$submissionId] = [];
}
if ($row->setting_name === 'coverImage') {
$coverImagesBySubmission[$submissionId]['uploadName'] = $row->setting_value;
$coverImagesBySubmission[$submissionId]['dateUploaded'] = Core::getCurrentDate();
} elseif ($row->setting_name === 'coverImageAltText') {
$coverImagesBySubmission[$submissionId]['altText'] = $row->setting_value;
}
}
foreach ($coverImagesBySubmission as $submissionId => $coverImagesBySubmission) {
$deprecatedDao->update(
'UPDATE submission_settings
SET setting_value = ?
WHERE submission_id = ? AND setting_name = ?',
[
serialize($coverImagesBySubmission),
$submissionId,
'coverImage',
]
);
}
return true;
}
/**
* Get the directory of a file based on its file stage
*
* @param int $fileStage One of SubmissionFile::SUBMISSION_FILE_ constants
*
* @return string
*/
public function _fileStageToPath($fileStage)
{
static $fileStagePathMap = [
SubmissionFile::SUBMISSION_FILE_SUBMISSION => 'submission',
SubmissionFile::SUBMISSION_FILE_NOTE => 'note',
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE => 'submission/review',
SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT => 'submission/review/attachment',
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION => 'submission/review/revision',
SubmissionFile::SUBMISSION_FILE_FINAL => 'submission/final',
SubmissionFile::SUBMISSION_FILE_COPYEDIT => 'submission/copyedit',
SubmissionFile::SUBMISSION_FILE_DEPENDENT => 'submission/proof',
SubmissionFile::SUBMISSION_FILE_PROOF => 'submission/proof',
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY => 'submission/productionReady',
SubmissionFile::SUBMISSION_FILE_ATTACHMENT => 'attachment',
SubmissionFile::SUBMISSION_FILE_QUERY => 'submission/query',
];
if (!isset($fileStagePathMap[$fileStage])) {
throw new \Exception('A file assigned to the file stage ' . $fileStage . ' could not be migrated.');
}
return $fileStagePathMap[$fileStage];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\install\Upgrade', '\Upgrade');
}
+464
View File
@@ -0,0 +1,464 @@
<?php
/**
* @file classes/issue/Collector.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Collector
*
* @brief A helper class to configure a query Builder to get a collection of issues
*/
namespace APP\issue;
use APP\facades\Repo;
use Exception;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use InvalidArgumentException;
use PKP\core\interfaces\CollectorInterface;
use PKP\core\PKPApplication;
use PKP\plugins\Hook;
class Collector implements CollectorInterface
{
public const ORDERBY_DATE_PUBLISHED = 'datePublished';
public const ORDERBY_LAST_MODIFIED = 'lastModified';
public const ORDERBY_SEQUENCE = 'seq';
public const ORDERBY_PUBLISHED_ISSUES = 'publishedIssues';
public const ORDERBY_UNPUBLISHED_ISSUES = 'unpublishedIssues';
public const ORDERBY_SHELF = 'shelf';
public const ORDER_DIR_ASC = 'ASC';
public const ORDER_DIR_DESC = 'DESC';
private const ORDER_CURRENT_ISSUE = 'currentIssue';
public DAO $dao;
public ?int $count = null;
public ?int $offset = null;
/** @var array|null Context ID or PKPApplication::CONTEXT_ID_ALL to get from all contexts */
public ?array $contextIds = null;
/** @var array|null List of issue IDs to include */
public ?array $issueIds = null;
/** @var array|null order and direction pairing for queries */
public ?array $resultOrderings = null;
/** @var bool|null return published issues */
public ?bool $isPublished = null;
/** @var array|null return issues in volume(s) */
public ?array $volumes = null;
/** @var array|null return issues with number(s) */
public ?array $numbers = null;
/** @var array|null return issues with year(s) */
public ?array $years = null;
/** @var array|null return issues that match a title */
public ?array $titles = null;
public ?array $doiStatuses = null;
public ?bool $hasDois = null;
/** @var array Which DOI types should be considered when checking if a submission has DOIs set */
public array $enabledDoiTypes = [];
/** @var string|null Returns Issue by URL path */
public ?string $urlPath = null;
/** @var string|null return issues which match words from this search phrase */
public ?string $searchPhrase = null;
public function __construct(DAO $dao)
{
$this->dao = $dao;
}
/** @copydoc DAO::getCount() */
public function getCount(): int
{
return $this->dao->getCount($this);
}
/**
* @copydoc DAO::getIds()
*
* @return Collection<int,int>
*/
public function getIds(): Collection
{
return $this->dao->getIds($this);
}
/**
* @copydoc DAO::getMany()
*
* @return LazyCollection<int,Issue>
*/
public function getMany(): LazyCollection
{
return $this->dao->getMany($this);
}
/**
* Set context issues filter
*
* @return $this
*/
public function filterByContextIds(?array $contextIds): static
{
$this->contextIds = $contextIds;
return $this;
}
/**
* Set issue ID filter
*
* @return $this
*/
public function filterByIssueIds(?array $issueIds): static
{
$this->issueIds = $issueIds;
return $this;
}
/**
* Set result order and direction based on an ORDERBY_* constant
*
* @return $this
*/
public function orderBy(string $orderByConstant): static
{
$this->resultOrderings = match ($orderByConstant) {
static::ORDERBY_LAST_MODIFIED => [
['orderBy' => 'i.last_modified', 'direction' => static::ORDER_DIR_DESC]
],
static::ORDERBY_SEQUENCE => [
['orderBy' => 'o.seq', 'direction' => static::ORDER_DIR_ASC]
],
static::ORDERBY_PUBLISHED_ISSUES => [
['orderBy' => 'o.seq', 'direction' => static::ORDER_DIR_ASC],
['orderBy' => 'currentIssue', 'direction' => static::ORDER_DIR_DESC],
['orderBy' => 'i.date_published', 'direction' => static::ORDER_DIR_DESC]
],
static::ORDERBY_UNPUBLISHED_ISSUES => [
['orderBy' => 'i.year', 'direction' => static::ORDER_DIR_ASC],
['orderBy' => 'i.volume', 'direction' => static::ORDER_DIR_ASC],
['orderBy' => 'i.number', 'direction' => static::ORDER_DIR_ASC]
],
static::ORDERBY_SHELF => [
['orderBy' => static::ORDER_CURRENT_ISSUE, 'direction' => static::ORDER_DIR_DESC],
['orderBy' => 'i.year', 'direction' => static::ORDER_DIR_ASC],
['orderBy' => 'i.volume', 'direction' => static::ORDER_DIR_ASC],
['orderBy' => 'i.number', 'direction' => static::ORDER_DIR_ASC]
],
default => throw new InvalidArgumentException('One of ORDERBY_* constants must be provided')
};
return $this;
}
/**
* Set published filter
*
* @return $this
*/
public function filterByPublished(bool $isPublished): static
{
$this->isPublished = $isPublished;
return $this;
}
/**
* Set volumes filter
*
* @param int[]|null $volumes
*
* @return $this
*/
public function filterByVolumes(?array $volumes): static
{
$this->volumes = $volumes;
return $this;
}
/**
* Set volumes filter
*
* @param int[]|null $numbers
*
* @return $this
*/
public function filterByNumbers(?array $numbers): static
{
$this->numbers = $numbers;
return $this;
}
/**
* Set volumes filter
*
* @param int[]|null $years
*
* @return $this
*/
public function filterByYears(?array $years): static
{
$this->years = $years;
return $this;
}
/**
* set urlPath filter
*
* @return $this
*/
public function filterByUrlPath(string $urlPath): static
{
$this->urlPath = $urlPath;
return $this;
}
/**
* Set titles filter
*
* @return $this
*/
public function filterByTitles(array $titles): static
{
$this->titles = $titles;
return $this;
}
/**
* Limit results to issues with these statuses
*
* @param array|null $statuses One or more of DOI::STATUS_* constants
*
*/
public function filterByDoiStatuses(?array $statuses): static
{
$this->doiStatuses = $statuses;
return $this;
}
/**
* Limit results to issues that do/don't have any DOIs assign to their sub objects
*
* @param array|null $enabledDoiTypes TYPE_* constants to consider when checking issue has DOIs
*
* @return $this
*/
public function filterByHasDois(?bool $hasDois, ?array $enabledDoiTypes = null): static
{
$this->hasDois = $hasDois;
$this->enabledDoiTypes = $enabledDoiTypes === null ? [Repo::doi()::TYPE_ISSUE] : $enabledDoiTypes;
return $this;
}
/**
* Set query search phrase
*
* @return $this
*/
public function searchPhrase(?string $phrase): static
{
$this->searchPhrase = $phrase;
return $this;
}
/**
* Limit the number of objects retrieved
*
* @return $this
*/
public function limit(?int $count): static
{
$this->count = $count;
return $this;
}
/**
* Offset the number of objects retrieved, for example to
* retrieve the second page of contents
*
* @return $this
*/
public function offset(?int $offset): static
{
$this->offset = $offset;
return $this;
}
/**
* @inheritDoc
*/
public function getQueryBuilder(): Builder
{
$q = DB::table($this->dao->table, 'i')
->select('i.*')
->leftJoin('custom_issue_orders as o', 'o.issue_id', '=', 'i.issue_id');
// Issue titles (exact matches)
$q->when(
$this->titles !== null,
fn (Builder $q) =>
$q->whereIn(
'i.issue_id',
fn (Builder $q) =>
$q->select('issue_id')
->from($this->dao->settingsTable)
->where('setting_name', '=', 'title')
->whereIn('setting_value', $this->titles)
)
);
// Context
// Never permit a query without a context_id unless the PKPApplication::CONTEXT_ID_ALL wildcard
// has been set explicitly.
if (!isset($this->contextIds)) {
throw new Exception('Submissions can not be retrieved without a context id. Pass the Application::CONTEXT_ID_ALL wildcard to get submissions from any context.');
} elseif (!in_array(PKPApplication::CONTEXT_ID_ALL, $this->contextIds)) {
$q->whereIn('i.journal_id', $this->contextIds);
}
// Issue IDs
$q->when($this->issueIds !== null, fn (Builder $q) => $q->whereIn('i.issue_id', $this->issueIds));
// Published
$q->when($this->isPublished !== null, fn (Builder $q) => $q->where('i.published', '=', $this->isPublished ? 1 : 0));
// Volumes
$q->when($this->volumes !== null, fn (Builder $q) => $q->whereIn('i.volume', $this->volumes));
// Numbers
$q->when($this->numbers !== null, fn (Builder $q) => $q->whereIn('i.number', $this->numbers));
// Years
$q->when($this->years !== null, fn (Builder $q) => $q->whereIn('i.year', $this->years));
// URL path
$q->when($this->urlPath !== null, fn (Builder $q) => $q->where('i.url_path', '=', $this->urlPath));
// DOI statuses
$q->when(
$this->doiStatuses !== null,
fn (Builder $q) =>
$q->whereIn(
'i.issue_id',
fn (Builder $q) =>
$q->select('i.issue_id')
->from('issues as i')
->leftJoin('dois as d', 'd.doi_id', '=', 'i.doi_id')
->whereIn('d.status', $this->doiStatuses)
)
);
// By whether issue has DOI assigned
$q->when(
$this->hasDois !== null,
fn (Builder $q) =>
$q->whereIn(
'i.issue_id',
fn (Builder $q) =>
$q->select('current_i.issue_id')
->from('issues', 'current_i')
->when(
in_array(Repo::doi()::TYPE_ISSUE, $this->enabledDoiTypes),
fn (Builder $q) =>
$this->hasDois ? $q->whereNotNull('current_i.doi_id') : $q->whereNull('current_i.doi_id')
)
)
);
// Search phrase
if ($this->searchPhrase !== null) {
$searchPhrase = $this->searchPhrase;
// Add support for searching for the volume, number and year
// using the localized issue identification formats. In
// en this will match Vol. 1. No. 1 (2018) against:
// i.volume = 1 AND i.number = 1 AND i.year = 2018
$volume = '';
$volumeRegex = '/\b' . preg_quote(__('issue.vol'), '/') . '\s+(\d+)/';
if (preg_match($volumeRegex, $searchPhrase, $matches)) {
[$found, $volume] = $matches;
$searchPhrase = str_replace($found, '', $searchPhrase);
}
$number = '';
$numberRegex = '/\b' . preg_quote(__('issue.no'), '/') . '\s+(\S+)\b/';
if (preg_match($numberRegex, $searchPhrase, $matches)) {
[$found, $number] = $matches;
$searchPhrase = str_replace($found, '', $searchPhrase);
}
$year = '';
if (preg_match('/\((\d{4})\)/', $searchPhrase, $matches)) {
[$found, $year] = $matches;
$searchPhrase = str_replace($found, '', $searchPhrase);
}
$q->when(
strlen($volume) || $number !== '' || $year !== '',
fn (Builder $q) => $q->where(
fn (Builder $q) => $q
->when($volume !== '', fn (Builder $q) => $q->where('i.volume', '=', $volume))
->when($number !== '', fn (Builder $q) => $q->where('i.number', '=', $number))
->when($year !== '', fn (Builder $q) => $q->where('i.year', '=', $year))
)
);
$words = array_filter(array_unique(explode(' ', $searchPhrase)), 'strlen');
if (count($words)) {
$likePattern = DB::raw("CONCAT('%', LOWER(?), '%')");
foreach ($words as $word) {
$q->where(
fn (Builder $q) => $q
->whereIn(
'i.issue_id',
fn (Builder $q) =>
$q->select('iss_t.issue_id')
->from($this->dao->settingsTable, 'iss_t')
->where('iss_t.setting_name', '=', 'title')
->where(DB::raw('LOWER(iss_t.setting_value)'), 'LIKE', $likePattern)->addBinding($word)
)
->orWhereIn(
'i.issue_id',
fn (Builder $q) =>
$q->select('iss_d.issue_id')
->from($this->dao->settingsTable, 'iss_d')
->where('iss_d.setting_name', '=', 'name')
->where(DB::raw('LOWER(iss_d.setting_value)'), 'LIKE', $likePattern)->addBinding($word)
)
// Match any four-digit number to the year
->when(ctype_digit($word) && strlen($word) === 4, fn (Builder $q) => $q->orWhere('i.year', '=', $word))
);
}
}
}
// Ordering for query-builder-based and legacy-based orderings
$q->when($this->resultOrderings !== null, function (Builder $q) {
foreach ($this->resultOrderings as $resultOrdering) {
if ($resultOrdering['orderBy'] === static::ORDER_CURRENT_ISSUE) {
// Custom query to order by current issue status from the journals table
$q->leftJoin('journals as j', 'j.current_issue_id', '=', 'i.issue_id')
->orderByRaw('CASE WHEN j.current_issue_id IS NOT NULL then 1 else 0 END ' . $resultOrdering['direction']);
} else {
$q->orderBy($resultOrdering['orderBy'], $resultOrdering['direction']);
}
}
});
// Limit and offset results for pagination
$q->when($this->count !== null, fn (Builder $q) => $q->limit($this->count));
$q->when($this->offset !== null, fn (Builder $q) => $q->offset($this->offset));
// Add app-specific query statements
Hook::call('Issue::getMany::queryObject', [&$q, $this]);
return $q;
}
}
+490
View File
@@ -0,0 +1,490 @@
<?php
/**
* @file classes/issue/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @ingroup issue
*
* @see Issue
*
* @brief Operations for retrieving and modifying Issue objects.
*/
namespace APP\issue;
use APP\facades\Repo;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\cache\CacheManager;
use PKP\cache\GenericCache;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
use PKP\db\DAOResultFactory;
use PKP\services\PKPSchemaService;
/**
* @extends EntityDAO<Issue>
*/
class DAO extends EntityDAO implements \PKP\plugins\PKPPubIdPluginDAO
{
/**
* @use EntityWithParent<Issue>
*/
use EntityWithParent;
// TODO: Needs to be addressed with refactor of caching.
public $caches;
/** @copydoc EntityDAO::$schema */
public $schema = PKPSchemaService::SCHEMA_ISSUE;
/** @copydoc EntityDAO::$table */
public $table = 'issues';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = 'issue_settings';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'issue_id';
/** @copydoc SchemaDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'issue_id',
'journalId' => 'journal_id',
'volume' => 'volume',
'number' => 'number',
'year' => 'year',
'published' => 'published',
'datePublished' => 'date_published',
'dateNotified' => 'date_notified',
'lastModified' => 'last_modified',
'accessStatus' => 'access_status',
'openAccessDate' => 'open_access_date',
'showVolume' => 'show_volume',
'showNumber' => 'show_number',
'showYear' => 'show_year',
'showTitle' => 'show_title',
'styleFileName' => 'style_file_name',
'originalStyleFileName' => 'original_style_file_name',
'urlPath' => 'url_path',
'doiId' => 'doi_id'
];
/**
* Get the parent object ID column name
*/
public function getParentColumn(): string
{
return 'journal_id';
}
/**
* Handle a cache miss.
*
* TODO: Caching not currently working as expected
*
*/
public function _cacheMiss(GenericCache $cache, int $id): ?Issue
{
if ($cache->getCacheId() === 'current') {
$issue = Repo::issue()->getCurrent($id);
} else {
$issue = Repo::issue()->getByBestId($id, null, false);
}
$cache->setCache($id, $issue);
return $issue;
}
/**
* Get an issue cache by cache ID
*
* TODO: Not currently working as expected. Not used throughout current class
*
* @return mixed|object|\PKP\cache\APCCache|\PKP\cache\FileCache|GenericCache|\PKP\cache\MemcacheCache|\PKP\cache\XCacheCache
*/
public function _getCache(string $cacheId)
{
if (!isset($this->caches)) {
$this->caches = [];
}
if (!isset($this->caches[$cacheId])) {
$cacheManager = CacheManager::getManager();
$this->caches[$cacheId] = $cacheManager->getObjectCache('issues', $cacheId, [$this, '_cacheMiss']);
}
return $this->caches[$cacheId];
}
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Issue
{
return app(Issue::class);
}
/**
* Get the number of announcements matching the configured query
*/
public function getCount(Collector $query): int
{
return $query
->getQueryBuilder()
->count();
}
/**
* Get a list of ids matching the configured query
*/
public function getIds(Collector $query): Collection
{
return $query
->getQueryBuilder()
->select('i.' . $this->primaryKeyColumn)
->pluck('i.' . $this->primaryKeyColumn);
}
/**
* Get a collection of issues matching the configured query
*
* @return LazyCollection<int,Issue>
*/
public function getMany(Collector $query): LazyCollection
{
$rows = $query
->getQueryBuilder()
->get();
return LazyCollection::make(function () use ($rows) {
foreach ($rows as $row) {
yield $row->issue_id => $this->fromRow($row);
}
});
}
/**
* Get the issue id by its url path
*
*/
public function getIdByUrlPath(string $urlPath, int $contextId): ?int
{
$issue = DB::table($this->table, 'i')
->where('i.journal_id', '=', $contextId)
->where('i.url_path', '=', $urlPath)
->first();
return $issue ? $issue->issue_id : null;
}
/** @copydoc EntityDAO::fromRow() */
public function fromRow(object $row): Issue
{
$issue = parent::fromRow($row);
$this->setDoiObject($issue);
return $issue;
}
/** @copydoc EntityDAO::_insert() */
public function insert(Issue $issue): int
{
$issueId = parent::_insert($issue);
$this->resequenceCustomIssueOrders($issue->getData('journalId'));
return $issueId;
}
/** @copydoc EntityDAO::_update() */
public function update(Issue $issue)
{
$issue->stampModified();
parent::_update($issue);
$this->resequenceCustomIssueOrders($issue->getData('journalId'));
// TODO: Flush cache
}
/** @copydoc EntityDAO::_delete() */
public function delete(Issue $issue)
{
parent::_delete($issue);
$this->resequenceCustomIssueOrders($issue->getData('journalId'));
// TODO: Flush cache
}
/**
* Deletes any custom issue ordering for a given context
*
*/
public function deleteCustomIssueOrdering(int $issueId)
{
DB::table('custom_issue_orders')
->where('issue_id', '=', $issueId)
->delete();
}
/**
* Sequentially renumber custom issue orderings in their sequence order.
*
*/
public function resequenceCustomIssueOrders(int $contextId)
{
// If no custom issue ordering already exists, there is nothing to do
if (!$this->customIssueOrderingExists($contextId)) {
return;
}
$results = DB::table($this->table, 'i')
->leftJoin('custom_issue_orders as o', 'o.issue_id', '=', 'i.issue_id')
->where('i.journal_id', '=', $contextId)
// TODO: Previous behaviour would resequence all issues, including those that haven't been published (cont.)
// artificially giving them a position in the custom_issue_orders table before they've been published.
->where('i.published', '=', 1)
->orderBy('o.seq')
->select('i.issue_id')
->get();
$results->each(function ($item, $key) use ($contextId) {
$newSeq = $key + 1;
DB::table('custom_issue_orders')
->updateOrInsert(
[
'issue_id' => $item->issue_id,
],
[
'issue_id' => $item->issue_id,
'journal_id' => (int) $contextId,
'seq' => $newSeq
],
);
});
}
/**
* Check if a journal has custom issue ordering
*
*/
public function customIssueOrderingExists(int $contextId): bool
{
$resultCount = DB::table('custom_issue_orders', 'o')
->where('o.journal_id', '=', $contextId)
->count();
return $resultCount != 0;
}
/**
* Get the custom issue order of a journal
*
*/
public function getCustomIssueOrder(int $contextId, int $issueId): ?int
{
$results = DB::table('custom_issue_orders')
->where('journal_id', '=', (int) $contextId)
->where('issue_id', '=', (int) $issueId);
$row = $results->first();
return $row ? (int) $row->seq : null;
}
/**
* Gets a list of all the years in which issues have been published
*
*/
public function getYearsIssuesPublished(int $contextId): Collection
{
$collector = Repo::issue()->getCollector();
$q = $collector->filterByContextIds([$contextId])
->filterByPublished(true)
->getQueryBuilder();
return $q->select('i.year')
->groupBy('i.year')
->orderBy('i.year', 'DESC')
->pluck('i.year');
}
/**
* INTERNAL USE ONLY: Insert a custom issue ordering
* TODO: See if should be protected/private
*
*/
public function insertCustomIssueOrder(int $contextId, int $issueId, int $seq)
{
DB::table('custom_issue_orders')
->insert(
[
'issue_id' => $issueId,
'journal_id' => $contextId,
'seq' => $seq
]
);
}
/**
* Move a custom issue ordering up or down, resequencing as necessary.
*
* @param int $newPos The new position (0-based) of this section
*/
public function moveCustomIssueOrder(int $contextId, int $issueId, int $newPos)
{
DB::table('custom_issue_orders')
->updateOrInsert(
[
'journal_id' => $contextId,
'issue_id' => $issueId
],
[
'seq' => $newPos
]
);
}
/**
* @copydoc PKPPubIdPluginDAO::pubIdExists()
*
* From legacy IssueDAO
*/
public function pubIdExists($pubIdType, $pubId, $excludePubObjectId, $contextId)
{
$result = $this->deprecatedDao->retrieve(
'SELECT COUNT(*) AS row_count
FROM issue_settings ist
INNER JOIN issues i ON ist.issue_id = i.issue_id
WHERE ist.setting_name = ? AND ist.setting_value = ? AND i.issue_id <> ? AND i.journal_id = ?',
[
'pub-id::' . $pubIdType,
$pubId,
(int) $excludePubObjectId,
(int) $contextId
]
);
$row = $result->current();
return $row && $row->row_count;
}
/**
* @copydoc PKPPubIdPluginDAO::changePubId()
*
* From legacy IssueDAO
*/
public function changePubId($pubObjectId, $pubIdType, $pubId)
{
DB::table('issue_settings')->updateOrInsert(
['issue_id' => (int) $pubObjectId, 'locale' => '', 'setting_name' => 'pub-id::' . $pubIdType],
['setting_value' => (string) $pubId]
);
// TODO: Cache not implemented
// $this->flushCache();
}
/**
* @copydoc PKPPubIdPluginDAO::deletePubId()
*
* From legacy IssueDAO
*/
public function deletePubId($pubObjectId, $pubIdType)
{
$this->deprecatedDao->update(
'DELETE FROM issue_settings WHERE setting_name = ? AND issue_id = ?',
[
'pub-id::' . $pubIdType,
(int)$pubObjectId
]
);
// TODO: Cache not implemented
// $this->flushCache();
}
/**
* @copydoc PKPPubIdPluginDAO::deleteAllPubIds()
*
* From legacy IssueDAO
*/
public function deleteAllPubIds($contextId, $pubIdType)
{
$issues = Repo::issue()->getCollector()->filterByContextIds([$contextId])->getMany();
foreach ($issues as $issue) {
$this->deprecatedDao->update(
'DELETE FROM issue_settings WHERE setting_name = ? AND issue_id = ?',
[
'pub-id::' . $pubIdType,
(int)$issue->getId()
]
);
}
// TODO: Cache not implemented
// $this->flushCache();
}
/**
* Get all published issues (eventually with a pubId assigned and) matching the specified settings.
*
* From legacy IssueDAO
*
* @param int $contextId optional
* @param string $pubIdType
* @param string $pubIdSettingName optional
* (e.g. crossref::registeredDoi)
* @param string $pubIdSettingValue optional
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Issue>
*/
public function getExportable($contextId, $pubIdType = null, $pubIdSettingName = null, $pubIdSettingValue = null, $rangeInfo = null)
{
$params = [];
if ($pubIdSettingName) {
$params[] = $pubIdSettingName;
}
$params[] = (int) $contextId;
if ($pubIdType) {
$params[] = 'pub-id::' . $pubIdType;
}
import('classes.plugins.PubObjectsExportPlugin'); // Constants
if ($pubIdSettingName && $pubIdSettingValue && $pubIdSettingValue != EXPORT_STATUS_NOT_DEPOSITED) {
$params[] = $pubIdSettingValue;
}
$result = $this->deprecatedDao->retrieveRange(
$sql = 'SELECT i.*
FROM issues i
LEFT JOIN custom_issue_orders o ON (o.issue_id = i.issue_id)
' . ($pubIdType != null ? ' LEFT JOIN issue_settings ist ON (i.issue_id = ist.issue_id)' : '')
. ($pubIdSettingName != null ? ' LEFT JOIN issue_settings iss ON (i.issue_id = iss.issue_id AND iss.setting_name = ?)' : '') . '
WHERE
i.published = 1 AND i.journal_id = ?
' . ($pubIdType != null ? ' AND ist.setting_name = ? AND ist.setting_value IS NOT NULL' : '')
. (($pubIdSettingName != null && $pubIdSettingValue != null && $pubIdSettingValue == EXPORT_STATUS_NOT_DEPOSITED) ? ' AND iss.setting_value IS NULL' : '')
. (($pubIdSettingName != null && $pubIdSettingValue != null && $pubIdSettingValue != EXPORT_STATUS_NOT_DEPOSITED) ? ' AND iss.setting_value = ?' : '')
. (($pubIdSettingName != null && is_null($pubIdSettingValue)) ? ' AND (iss.setting_value IS NULL OR iss.setting_value = \'\')' : '')
. ' ORDER BY i.date_published DESC',
$params,
$rangeInfo
);
return new DAOResultFactory($result, $this, 'fromRow', [], $sql, $params, $rangeInfo);
}
/**
* Flush the issue cache.
*
* TODO: Not currently in use. _getCache always results in cache miss.
*/
public function flushCache()
{
$this->_getCache('issues')->flush();
$this->_getCache('current')->flush();
}
/**
* Set the DOI object
*
*/
protected function setDoiObject(Issue $issue)
{
if (!empty($issue->getData('doiId'))) {
$issue->setData('doiObject', Repo::doi()->get($issue->getData('doiId')));
}
}
}
+723
View File
@@ -0,0 +1,723 @@
<?php
/**
* @defgroup issue Issue
* Implement journal issues.
*/
/**
* @file classes/issue/Issue.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Issue
*
* @ingroup issue
*
* @see \APP\issue\DAO
*
* @brief Class for Issue.
*/
namespace APP\issue;
use APP\core\Application;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use PKP\core\Core;
use PKP\facades\Locale;
use PKP\submission\PKPSubmission;
class Issue extends \PKP\core\DataObject
{
public const ISSUE_ACCESS_OPEN = 1;
public const ISSUE_ACCESS_SUBSCRIPTION = 2;
/**
* get journal id
*
* @return int
*/
public function getJournalId()
{
return $this->getData('journalId');
}
/**
* set journal id
*
* @param int $journalId
*/
public function setJournalId($journalId)
{
return $this->setData('journalId', $journalId);
}
/**
* Get the localized title
*
* @return string
*/
public function getLocalizedTitle()
{
return $this->getLocalizedData('title');
}
/**
* get title
*
* @param string $locale
*
* @return string|array<string,string>
*/
public function getTitle($locale)
{
return $this->getData('title', $locale);
}
/**
* set title
*
* @param string $title
* @param string $locale
*/
public function setTitle($title, $locale)
{
return $this->setData('title', $title, $locale);
}
/**
* get volume
*
* @return int
*/
public function getVolume()
{
return $this->getData('volume');
}
/**
* set volume
*
* @param int $volume
*/
public function setVolume($volume)
{
return $this->setData('volume', $volume);
}
/**
* get number
*
* @return string
*/
public function getNumber()
{
return $this->getData('number');
}
/**
* set number
*
* @param string $number
*/
public function setNumber($number)
{
return $this->setData('number', $number);
}
/**
* get year
*
* @return int
*/
public function getYear()
{
return $this->getData('year');
}
/**
* set year
*
* @param int $year
*/
public function setYear($year)
{
return $this->setData('year', $year);
}
/**
* get published
*
* @return int
*/
public function getPublished()
{
return $this->getData('published');
}
/**
* set published
*
* @param int $published
*/
public function setPublished($published)
{
return $this->setData('published', $published);
}
/**
* get date published
*
* @return string
*/
public function getDatePublished()
{
return $this->getData('datePublished');
}
/**
* set date published
*
* @param string $datePublished
*/
public function setDatePublished($datePublished)
{
return $this->setData('datePublished', $datePublished);
}
/**
* get date the users were last notified
*
* @return string
*/
public function getDateNotified()
{
return $this->getData('dateNotified');
}
/**
* set date the users were last notified
*
* @param string $dateNotified
*/
public function setDateNotified($dateNotified)
{
return $this->setData('dateNotified', $dateNotified);
}
/**
* get date the issue was last modified
*
* @return string
*/
public function getLastModified()
{
return $this->getData('lastModified');
}
/**
* set date the issue was last modified
*
* @param string $lastModified
*/
public function setLastModified($lastModified)
{
return $this->setData('lastModified', $lastModified);
}
/**
* Stamp the date of the last modification to the current time.
*/
public function stampModified()
{
return $this->setLastModified(Core::getCurrentDate());
}
/**
* get access status (ISSUE_ACCESS_...)
*
* @return int
*/
public function getAccessStatus()
{
return $this->getData('accessStatus');
}
/**
* set access status (ISSUE_ACCESS_...)
*
* @param int $accessStatus
*/
public function setAccessStatus($accessStatus)
{
return $this->setData('accessStatus', $accessStatus);
}
/**
* get open access date
*
* @return string
*/
public function getOpenAccessDate()
{
return $this->getData('openAccessDate');
}
/**
* set open access date
*
* @param string $openAccessDate
*/
public function setOpenAccessDate($openAccessDate)
{
return $this->setData('openAccessDate', $openAccessDate);
}
/**
* Get the localized description
*
* @return string
*/
public function getLocalizedDescription()
{
return $this->getLocalizedData('description');
}
/**
* get description
*
* @param string $locale
*
* @return string|array<string,string>
*/
public function getDescription($locale)
{
return $this->getData('description', $locale);
}
/**
* set description
*
* @param string $description
* @param string $locale
*/
public function setDescription($description, $locale)
{
return $this->setData('description', $description, $locale);
}
/**
* Returns current DOI
*
*/
public function getDoi(): ?string
{
$doiObject = $this->getData('doiObject');
if (empty($doiObject)) {
return null;
} else {
return $doiObject->getData('doi');
}
}
/**
* Get stored public ID of the issue.
*
* This helper function is required by PKPPubIdPlugins.
* NB: To maintain backwards compatibility, getDoi() is called from here
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
*
* @return string
*/
public function getStoredPubId($pubIdType)
{
if ($pubIdType == 'doi') {
return $this->getDoi();
} else {
return $this->getData('pub-id::' . $pubIdType);
}
}
/**
* Set stored public issue id.
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
*/
public function setStoredPubId($pubIdType, $pubId)
{
if ($pubIdType == 'doi') {
if ($doiObject = $this->getData('doiObject')) {
Repo::doi()->edit($doiObject, ['doi' => $pubId]);
} else {
$newDoiObject = Repo::doi()->newDataObject(
[
'doi' => $pubId,
'contextId' => $this->getJournalId()
]
);
$doiId = Repo::doi()->add($newDoiObject);
$this->setData('doiId', $doiId);
}
} else {
$this->setData('pub-id::' . $pubIdType, $pubId);
}
}
/**
* get show issue volume
*
* @return int
*/
public function getShowVolume()
{
return $this->getData('showVolume');
}
/**
* set show issue volume
*
* @param int $showVolume
*/
public function setShowVolume($showVolume)
{
return $this->setData('showVolume', $showVolume);
}
/**
* get show issue number
*
* @return int
*/
public function getShowNumber()
{
return $this->getData('showNumber');
}
/**
* set show issue number
*
* @param int $showNumber
*/
public function setShowNumber($showNumber)
{
return $this->setData('showNumber', $showNumber);
}
/**
* get show issue year
*
* @return int
*/
public function getShowYear()
{
return $this->getData('showYear');
}
/**
* set show issue year
*
* @param int $showYear
*/
public function setShowYear($showYear)
{
return $this->setData('showYear', $showYear);
}
/**
* get show issue title
*
* @return int
*/
public function getShowTitle()
{
return $this->getData('showTitle');
}
/**
* set show issue title
*
* @param int $showTitle
*/
public function setShowTitle($showTitle)
{
return $this->setData('showTitle', $showTitle);
}
/**
* Get the localized issue cover image file name
*
* @return string
*/
public function getLocalizedCoverImage()
{
return $this->getLocalizedData('coverImage');
}
/**
* Get issue cover image file name
*
* @param string $locale
*
* @return string|array
*/
public function getCoverImage($locale)
{
return $this->getData('coverImage', $locale);
}
/**
* Set issue cover image file name
*
* @param string|array $coverImage
* @param string $locale
*/
public function setCoverImage($coverImage, $locale)
{
return $this->setData('coverImage', $coverImage, $locale);
}
/**
* Get the localized issue cover image alternate text
*
* @return string
*/
public function getLocalizedCoverImageAltText()
{
return $this->getLocalizedData('coverImageAltText');
}
/**
* Get issue cover image alternate text
*
* @param string $locale
*
* @return string
*/
public function getCoverImageAltText($locale)
{
return $this->getData('coverImageAltText', $locale);
}
/**
* Get a full URL to the localized cover image
*
* @return string
*/
public function getLocalizedCoverImageUrl()
{
$coverImage = $this->getLocalizedCoverImage();
if (!$coverImage) {
return '';
}
$request = Application::get()->getRequest();
$publicFileManager = new PublicFileManager();
return $request->getBaseUrl() . '/' . $publicFileManager->getContextFilesPath($this->getJournalId()) . '/' . $coverImage;
}
/**
* Get the full URL to all localized cover images
*
* @return array
*/
public function getCoverImageUrls()
{
$coverImages = $this->getCoverImage(null);
if (empty($coverImages)) {
return [];
}
$request = Application::get()->getRequest();
$publicFileManager = new PublicFileManager();
$urls = [];
foreach ($coverImages as $locale => $coverImage) {
$urls[$locale] = sprintf('%s/%s/%s', $request->getBaseUrl(), $publicFileManager->getContextFilesPath($this->getJournalId()), $coverImage);
}
return $urls;
}
/**
* Set issue cover image alternate text
*
* @param string $coverImageAltText
* @param string $locale
*/
public function setCoverImageAltText($coverImageAltText, $locale)
{
return $this->setData('coverImageAltText', $coverImageAltText, $locale);
}
/**
* Return the string of the issue identification based label format
*
* @param array $force force show/hide of data components
* @param string $locale use specific non-default locale
*
* @return string
*/
public function getIssueIdentification($force = [], $locale = null)
{
$displayOptions = [
'showVolume' => $this->getData('showVolume'),
'showNumber' => $this->getData('showNumber'),
'showYear' => $this->getData('showYear'),
'showTitle' => $this->getData('showTitle'),
];
$displayOptions = array_merge($displayOptions, $force);
if (is_null($locale)) {
$locale = Locale::getLocale();
}
$volLabel = __('issue.vol', [], $locale);
$numLabel = __('issue.no', [], $locale);
$vol = $this->getData('volume');
$num = $this->getData('number');
$year = $this->getData('year');
$title = $this->getTitle($locale);
if (empty($title)) {
$title = $this->getLocalizedTitle();
}
$identification = [];
foreach ($displayOptions as $opt => $val) {
if (empty($val)) {
continue;
}
if ($opt == 'showVolume') {
$identification[] = "{$volLabel} {$vol}";
} elseif ($opt == 'showNumber') {
$identification[] = "{$numLabel} {$num}";
} elseif ($opt == 'showYear') {
$identification[] = !empty($identification) ? "({$year})" : $year;
} elseif ($opt == 'showTitle') {
if (!empty($title)) {
// Append a separator to the last key
if (!empty($identification)) {
end($identification);
$identification[key($identification)] .= ':';
}
$identification[] = $title;
}
}
}
// If we've got an empty title, re-run the function and force a result
if (empty($identification)) {
return $this->getIssueIdentification(
[
'showVolume' => true,
'showNumber' => true,
'showYear' => true,
'showTitle' => false,
],
$locale
);
}
return join(' ', $identification);
}
/**
* Return the string of the issue series identification
* eg: Vol 1 No 1 (2000)
*
* @return string
*/
public function getIssueSeries()
{
if ($this->getShowVolume() || $this->getShowNumber() || $this->getShowYear()) {
return $this->getIssueIdentification(['showTitle' => false]);
}
return null;
}
/**
* Get number of articles in this issue.
*
* @return int
*/
public function getNumArticles()
{
return Repo::submission()->getCollector()
->filterByContextIds([$this->getData('journalId')])
->filterByIssueIds([$this->getId()])
->filterByStatus([PKPSubmission::STATUS_SCHEDULED, PKPSubmission::STATUS_PUBLISHED])
->getCount();
}
/**
* Return the "best" issue ID -- If a public issue ID is set,
* use it; otherwise use the internal issue Id.
*
* @return string
*/
public function getBestIssueId()
{
return strlen($urlPath = (string) $this->getData('urlPath')) ? $urlPath : $this->getId();
}
/**
* Check whether a description exists for this issue
*
* @return bool
*/
public function hasDescription()
{
$description = $this->getLocalizedDescription();
return !empty($description);
}
/**
* Checks whether issue had DOI assigned to it
*
*/
public function hasDoi(): bool
{
return (bool) $this->getData('doiObject');
}
/**
* @copydoc \PKP\core\DataObject::getDAO()
*/
public function getDAO(): DAO
{
return Repo::issue()->dao;
}
/**
* Display the object in Import/Export results
*
* @return string A string that Identifies the object
*/
public function getUIDisplayString()
{
return __('plugins.importexport.issue.cli.display', ['issueId' => $this->getId(), 'issueIdentification' => $this->getIssueIdentification()]);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\Issue', '\Issue');
foreach ([
'ISSUE_ACCESS_OPEN',
'ISSUE_ACCESS_SUBSCRIPTION',
] as $constantName) {
define($constantName, constant('\Issue::' . $constantName));
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
/**
* @file classes/issue/IssueAction.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueAction
*
* @ingroup issue
*
* @see Issue
*
* @brief IssueAction class.
*/
namespace APP\issue;
use APP\facades\Repo;
use APP\subscription\IndividualSubscriptionDAO;
use APP\subscription\InstitutionalSubscriptionDAO;
use APP\subscription\Subscription;
use PKP\db\DAORegistry;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\security\RoleDAO;
use PKP\submission\PKPSubmission;
class IssueAction
{
/**
* Actions.
*/
/**
* Checks if subscription is required for viewing the issue
*
* @param Issue $issue
* @param \APP\journal\Journal $journal
*
* @return bool
*/
public function subscriptionRequired($issue, $journal)
{
assert($issue instanceof \APP\issue\Issue);
assert($journal instanceof \APP\journal\Journal);
assert($journal->getId() == $issue->getJournalId());
// Check subscription state.
$result = $journal->getData('publishingMode') == \APP\journal\Journal::PUBLISHING_MODE_SUBSCRIPTION &&
$issue->getAccessStatus() != \APP\issue\Issue::ISSUE_ACCESS_OPEN && (
is_null($issue->getOpenAccessDate()) ||
strtotime($issue->getOpenAccessDate()) > time()
);
Hook::call('IssueAction::subscriptionRequired', [&$journal, &$issue, &$result]);
return $result;
}
/**
* Checks if this user is granted access to pre-publication issue galleys
* based on their roles in the journal (i.e. Manager, Editor, etc).
*
* @param \APP\journal\Journal $journal
*
* @return bool
*/
public function allowedIssuePrePublicationAccess($journal, $user)
{
/** @var RoleDAO */
$roleDao = DAORegistry::getDAO('RoleDAO');
if ($user && $journal) {
$journalId = $journal->getId();
$userId = $user->getId();
$subscriptionAssumedRoles = [
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
Role::ROLE_ID_SUBSCRIPTION_MANAGER
];
$roles = $roleDao->getByUserId($userId, $journalId);
foreach ($roles as $role) {
if (in_array($role->getRoleId(), $subscriptionAssumedRoles)) {
return true;
}
}
}
return false;
}
/**
* Checks if user has subscription
*
* @param \PKP\user\User $user
* @param \APP\journal\Journal $journal
* @param int $issueId Issue ID (optional)
* @param int $articleId Article ID (optional)
*
* @return bool
*/
public function subscribedUser($user, $journal, $issueId = null, $articleId = null)
{
$subscriptionDao = DAORegistry::getDAO('IndividualSubscriptionDAO'); /** @var IndividualSubscriptionDAO $subscriptionDao */
$submission = Repo::submission()->get((int) $articleId);
$result = false;
if (isset($user) && isset($journal)) {
if ($submission && Repo::submission()->canPreview($user, $submission)) {
$result = true;
} else {
$result = $subscriptionDao->isValidIndividualSubscription($user->getId(), $journal->getId());
}
// If no valid subscription, check if there is an expired subscription
// that was valid during publication date of any one of the submission's
// publications
if (!$result && $journal->getData('subscriptionExpiryPartial')) {
if (isset($submission) && !empty($submission->getData('publications'))) {
foreach ($submission->getData('publications') as $publication) {
if ($subscriptionDao->isValidIndividualSubscription($user->getId(), $journal->getId(), Subscription::SUBSCRIPTION_DATE_END, $publication->getData('datePublished'))) {
$result = true;
break;
}
}
} elseif (isset($issueId)) {
$issue = Repo::issue()->get($issueId);
if (isset($issue) && $issue->getPublished()) {
$result = $subscriptionDao->isValidIndividualSubscription($user->getId(), $journal->getId(), Subscription::SUBSCRIPTION_DATE_END, $issue->getDatePublished());
}
}
}
}
Hook::call('IssueAction::subscribedUser', [&$user, &$journal, &$issueId, &$articleId, &$result]);
return $result;
}
/**
* Checks if remote client domain or ip is allowed
*
* @param \APP\core\Request $request
* @param \APP\journal\Journal $journal
* @param int $issueId Issue ID (optional)
* @param int $articleId Article ID (optional)
*
* @return bool
*/
public function subscribedDomain($request, $journal, $issueId = null, $articleId = null)
{
$subscriptionDao = DAORegistry::getDAO('InstitutionalSubscriptionDAO'); /** @var InstitutionalSubscriptionDAO $subscriptionDao */
$result = false;
if (isset($journal)) {
$result = $subscriptionDao->isValidInstitutionalSubscription($request->getRemoteDomain(), $request->getRemoteAddr(), $journal->getId());
// If no valid subscription, check if there is an expired subscription
// that was valid during publication date of requested content
if (!$result && $journal->getData('subscriptionExpiryPartial')) {
if (isset($articleId)) {
$submission = Repo::submission()->get($articleId);
if ($submission->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
$result = $subscriptionDao->isValidInstitutionalSubscription($request->getRemoteDomain(), $request->getRemoteAddr(), $journal->getId(), Subscription::SUBSCRIPTION_DATE_END, $submission->getDatePublished());
}
} elseif (isset($issueId)) {
$issue = Repo::issue()->get($issueId);
if (isset($issue) && $issue->getPublished()) {
$result = $subscriptionDao->isValidInstitutionalSubscription($request->getRemoteDomain(), $request->getRemoteAddr(), $journal->getId(), Subscription::SUBSCRIPTION_DATE_END, $issue->getDatePublished());
}
}
}
}
Hook::call('IssueAction::subscribedDomain', [&$request, &$journal, &$issueId, &$articleId, &$result]);
return (bool) $result;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\IssueAction', '\IssueAction');
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* @file classes/issue/IssueFile.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueFile
*
* @ingroup issue
*
* @brief Issue file class.
*/
namespace APP\issue;
use PKP\file\PKPFile;
class IssueFile extends PKPFile
{
/** @var int File content type IDs */
public const ISSUE_FILE_PUBLIC = 1;
//
// Get/set methods
//
/**
* Get ID of issue.
*
* @return int
*/
public function getIssueId()
{
return $this->getData('issueId');
}
/**
* set ID of issue.
*
* @param int $issueId
*/
public function setIssueId($issueId)
{
return $this->setData('issueId', $issueId);
}
/**
* Get content type of the file.
*
* @return string
*/
public function getContentType()
{
return $this->getData('contentType');
}
/**
* set type of the file.
*/
public function setContentType($contentType)
{
return $this->setData('contentType', $contentType);
}
/**
* Get modified date of file.
*
* @return string
*/
public function getDateModified()
{
return $this->getData('dateModified');
}
/**
* set modified date of file.
*
* @param string $dateModified
*/
public function setDateModified($dateModified)
{
return $this->setData('dateModified', $dateModified);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\IssueFile', '\IssueFile');
define('ISSUE_FILE_PUBLIC', IssueFile::ISSUE_FILE_PUBLIC);
}
+211
View File
@@ -0,0 +1,211 @@
<?php
/**
* @file classes/issue/IssueFileDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueFileDAO
*
* @ingroup issue
*
* @see IssueFile
*
* @brief Operations for retrieving and modifying IssueFile objects.
*/
namespace APP\issue;
use PKP\db\DAO;
use PKP\plugins\Hook;
class IssueFileDAO extends DAO
{
/** @var array MIME types that can be displayed inline in a browser */
public $_inlineableTypes = null;
/**
* Get inlineable file types.
*
* @return array
*/
public function getInlineableTypes()
{
return $this->_inlineableTypes;
}
/**
* Set inlineable file types.
*
* @param array $inlineableTypes
*/
public function setInlineableTypes($inlineableTypes)
{
$this->_inlineableTypes = $inlineableTypes;
}
/**
* Retrieve an issue file by ID.
*
* @param int $fileId
* @param int $issueId optional
*
* @return IssueFile
*/
public function getById($fileId, $issueId = null)
{
$params = [(int) $fileId];
if ($issueId) {
$params[] = (int) $issueId;
}
$result = $this->retrieve(
'SELECT f.*
FROM issue_files f
WHERE f.file_id = ?
' . ($issueId ? ' AND f.issue_id = ?' : ''),
$params
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Construct a new IssueFile data object.
*
* @return IssueFile
*/
public function newDataObject()
{
return new IssueFile();
}
/**
* Internal function to return an IssueFile object from a row.
*
* @param array $row
*
* @return IssueFile
*/
public function _fromRow($row)
{
$issueFile = $this->newDataObject();
$issueFile->setId($row['file_id']);
$issueFile->setIssueId($row['issue_id']);
$issueFile->setServerFileName($row['file_name']);
$issueFile->setFileType($row['file_type']);
$issueFile->setFileSize($row['file_size']);
$issueFile->setContentType($row['content_type']);
$issueFile->setOriginalFileName($row['original_file_name']);
$issueFile->setDateUploaded($this->datetimeFromDB($row['date_uploaded']));
$issueFile->setDateModified($this->datetimeFromDB($row['date_modified']));
Hook::call('IssueFileDAO::_returnIssueFileFromRow', [&$issueFile, &$row]);
return $issueFile;
}
/**
* Insert a new IssueFile.
*
* @param IssueFile $issueFile
*
* @return int
*/
public function insertObject($issueFile)
{
$this->update(
sprintf(
'INSERT INTO issue_files
(issue_id,
file_name,
file_type,
file_size,
content_type,
original_file_name,
date_uploaded,
date_modified)
VALUES
(?, ?, ?, ?, ?, ?, %s, %s)',
$this->datetimeToDB($issueFile->getDateUploaded()),
$this->datetimeToDB($issueFile->getDateModified())
),
[
(int) $issueFile->getIssueId(),
$issueFile->getServerFileName(),
$issueFile->getFileType(),
$issueFile->getFileSize(),
$issueFile->getContentType(),
$issueFile->getOriginalFileName()
]
);
$issueFile->setId($this->getInsertId());
return $issueFile->getId();
}
/**
* Update an existing issue file.
*/
public function updateObject($issueFile)
{
$this->update(
sprintf(
'UPDATE issue_files
SET
issue_id = ?,
file_name = ?,
file_type = ?,
file_size = ?,
content_type = ?,
original_file_name = ?,
date_uploaded = %s,
date_modified = %s
WHERE file_id = ?',
$this->datetimeToDB($issueFile->getDateUploaded()),
$this->datetimeToDB($issueFile->getDateModified())
),
[
(int) $issueFile->getIssueId(),
$issueFile->getServerFileName(),
$issueFile->getFileType(),
$issueFile->getFileSize(),
$issueFile->getContentType(),
$issueFile->getOriginalFileName(),
(int) $issueFile->getId()
]
);
return $issueFile->getId();
}
/**
* Delete an issue file.
*/
public function deleteObject($issueFile)
{
$this->deleteById($issueFile->getId());
}
/**
* Delete an issue file by ID.
*/
public function deleteById($fileId)
{
$this->update('DELETE FROM issue_files WHERE file_id = ?', [(int) $fileId]);
}
/**
* Delete all issue files for an issue.
*
* @param int $issueId
*/
public function deleteByIssueId($issueId)
{
$this->update('DELETE FROM issue_files WHERE issue_id = ?', [(int) $issueId]);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\IssueFileDAO', '\IssueFileDAO');
}
+229
View File
@@ -0,0 +1,229 @@
<?php
/**
* @defgroup issue_galley Issue Galleys
* Issue galleys allow for the representation of an entire journal issue with
* a single file, typically a PDF.
*/
/**
* @file classes/issue/IssueGalley.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueGalley
*
* @ingroup issue_galley
*
* @see IssueGalleyDAO
*
* @brief A galley is a final presentation version of the full-text of an issue.
*/
namespace APP\issue;
use APP\core\Application;
use APP\core\Services;
use APP\statistics\StatisticsHelper;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
class IssueGalley extends IssueFile
{
/** @var IssueFile */
public $_issueFile;
/**
* Check if galley is a PDF galley.
*
* @return bool
*/
public function isPdfGalley()
{
switch ($this->getFileType()) {
case 'application/pdf':
case 'application/x-pdf':
case 'text/pdf':
case 'text/x-pdf':
return true;
default: return false;
}
}
//
// Get/set methods
//
/**
* Get views count.
*
* @deprecated 3.4
*
* @return int
*/
public function getViews()
{
$filters = [
'dateStart' => StatisticsHelper::STATISTICS_EARLIEST_DATE,
'dateEnd' => date('Y-m-d', strtotime('yesterday')),
'contextIds' => [Application::get()->getRequest()->getContext()->getId()],
'issueGalleyIds' => [$this->getId()],
];
$metrics = Services::get('issueStats')
->getQueryBuilder($filters)
->getSum([])
->value('metric');
return $metrics ? $metrics : 0;
}
/**
* Get the localized value of the galley label.
*
* @return $string
*/
public function getGalleyLabel()
{
$label = $this->getLabel();
if ($this->getLocale() != Locale::getLocale()) {
$label .= ' (' . Locale::getMetadata($this->getLocale())->getDisplayName() . ')';
}
return $label;
}
/**
* Get label/title.
*
* @return string
*/
public function getLabel()
{
return $this->getData('label');
}
/**
* Set label/title.
*
* @param string $label
*/
public function setLabel($label)
{
return $this->setData('label', $label);
}
/**
* Get locale.
*
* @return string
*/
public function getLocale()
{
return $this->getData('locale');
}
/**
* Set locale.
*
* @param string $locale
*/
public function setLocale($locale)
{
return $this->setData('locale', $locale);
}
/**
* Get sequence order.
*
* @return float
*/
public function getSequence()
{
return $this->getData('sequence');
}
/**
* Set sequence order.
*
* @param float $sequence
*/
public function setSequence($sequence)
{
return $this->setData('sequence', $sequence);
}
/**
* Get file ID.
*
* @return int
*/
public function getFileId()
{
return $this->getData('fileId');
}
/**
* Set file ID.
*
* @param int $fileId
*/
public function setFileId($fileId)
{
return $this->setData('fileId', $fileId);
}
/**
* Get stored public ID of the galley.
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
*
* @return string
*/
public function getStoredPubId($pubIdType)
{
return $this->getData('pub-id::' . $pubIdType);
}
/**
* Set stored public galley id.
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
*/
public function setStoredPubId($pubIdType, $pubId)
{
return $this->setData('pub-id::' . $pubIdType, $pubId);
}
/**
* Return the "best" issue galley ID -- If a urlPath is set,
* use it; otherwise use the internal article Id.
*
* @return string
*/
public function getBestGalleyId()
{
return strlen($urlPath = (string) $this->getData('urlPath')) ? $urlPath : $this->getId();
}
/**
* Get the file corresponding to this galley.
*
* @return IssueFile
*/
public function getFile()
{
if (!isset($this->_issueFile)) {
$issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
$this->_issueFile = $issueFileDao->getById($this->getFileId());
}
return $this->_issueFile;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\IssueGalley', '\IssueGalley');
}
+438
View File
@@ -0,0 +1,438 @@
<?php
/**
* @file classes/issue/IssueGalleyDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueGalleyDAO
*
* @ingroup issue_galley
*
* @see IssueGalley
*
* @brief Operations for retrieving and modifying IssueGalley objects.
*/
namespace APP\issue;
use APP\file\IssueFileManager;
use PKP\plugins\Hook;
class IssueGalleyDAO extends \PKP\db\DAO
{
/**
* Retrieve a galley by ID.
*
* @param int $galleyId
* @param int $issueId optional
*
* @return IssueGalley
*/
public function getById($galleyId, $issueId = null)
{
$params = [(int) $galleyId];
if ($issueId !== null) {
$params[] = (int) $issueId;
}
$result = $this->retrieve(
'SELECT
g.*,
f.file_name,
f.original_file_name,
f.file_type,
f.file_size,
f.content_type,
f.date_uploaded,
f.date_modified
FROM issue_galleys g
LEFT JOIN issue_files f ON (g.file_id = f.file_id)
WHERE g.galley_id = ?' .
($issueId !== null ? ' AND g.issue_id = ?' : ''),
$params
);
$returner = null;
if ($row = $result->current()) {
$returner = $this->_fromRow((array) $row);
} else {
Hook::call('IssueGalleyDAO::getById', [&$galleyId, &$issueId, &$returner]);
}
return $returner;
}
/**
* Checks if public identifier exists (other than for the specified
* galley ID, which is treated as an exception).
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
* @param int $galleyId An ID to be excluded from the search.
* @param int $journalId
*
* @return bool
*/
public function pubIdExists($pubIdType, $pubId, $galleyId, $journalId)
{
$result = $this->retrieve(
'SELECT COUNT(*) AS row_count
FROM issue_galley_settings igs
INNER JOIN issue_galleys ig ON igs.galley_id = ig.galley_id
INNER JOIN issues i ON ig.issue_id = i.issue_id
WHERE igs.setting_name = ? AND igs.setting_value = ? AND igs.galley_id <> ? AND i.journal_id = ?',
[
'pub-id::' . $pubIdType,
$pubId,
(int) $galleyId,
(int) $journalId
]
);
$row = $result->current();
return $row ? (bool) $row->row_count : false;
}
/**
* Retrieve a galley by ID.
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
* @param int $issueId
*
* @return IssueGalley
*/
public function getByPubId($pubIdType, $pubId, $issueId)
{
$result = $this->retrieve(
'SELECT
g.*,
f.file_name,
f.original_file_name,
f.file_type,
f.file_size,
f.content_type,
f.date_uploaded,
f.date_modified
FROM issue_galleys g
INNER JOIN issue_galley_settings gs ON g.galley_id = gs.galley_id
LEFT JOIN issue_files f ON (g.file_id = f.file_id)
WHERE gs.setting_name = ? AND
gs.setting_value = ? AND
g.issue_id = ?',
['pub-id::' . $pubIdType, (string) $pubId, (int) $issueId]
);
$row = $result->current();
$returner = null;
if ($row) {
$returner = $this->_fromRow((array) $row);
} else {
Hook::call('IssueGalleyDAO::getByPubId', [&$pubIdType, &$pubId, &$issueId, &$returner]);
}
return $returner;
}
/**
* Retrieve all galleys for an issue.
*
* @param int $issueId
*
* @return array<int,IssueGalley> IssueGalleys
*/
public function getByIssueId($issueId)
{
$result = $this->retrieve(
'SELECT
g.*,
f.file_name,
f.original_file_name,
f.file_type,
f.file_size,
f.content_type,
f.date_uploaded,
f.date_modified
FROM issue_galleys g
LEFT JOIN issue_files f ON (g.file_id = f.file_id)
WHERE g.issue_id = ? ORDER BY g.seq',
[(int) $issueId]
);
$galleys = [];
foreach ($result as $row) {
$issueGalley = $this->_fromRow((array) $row);
$galleys[$issueGalley->getId()] = $issueGalley;
}
Hook::call('IssueGalleyDAO::getGalleysByIssue', [&$galleys, &$issueId]);
return $galleys;
}
/**
* Retrieve issue galley by urlPath or, failing that,
* internal galley ID; urlPath takes precedence.
*
* @param string $galleyId
* @param int $issueId
*
* @return IssueGalley object
*/
public function getByBestId($galleyId, $issueId)
{
$result = $this->retrieve(
'SELECT
g.*,
f.file_name,
f.original_file_name,
f.file_type,
f.file_size,
f.content_type,
f.date_uploaded,
f.date_modified
FROM issue_galleys g
LEFT JOIN issue_files f ON (g.file_id = f.file_id)
WHERE g.url_path = ? AND
g.issue_id = ?',
[
$galleyId,
(int) $issueId,
]
);
if ($row = $result->current()) {
return $this->_fromRow((array) $row);
}
return $this->getById($galleyId, $issueId);
}
/**
* Get the list of fields for which data is localized.
*
* @return array
*/
public function getLocaleFieldNames()
{
return [];
}
/**
* Get a list of additional fields that do not have
* dedicated accessors.
*
* @return array
*/
public function getAdditionalFieldNames()
{
$additionalFields = parent::getAdditionalFieldNames();
// FIXME: Move this to a PID plug-in.
$additionalFields[] = 'pub-id::publisher-id';
return $additionalFields;
}
/**
* Update the localized fields for this galley.
*
* @param IssueGalley $galley
*/
public function updateLocaleFields($galley)
{
$this->updateDataObjectSettings('issue_galley_settings', $galley, [
'galley_id' => $galley->getId()
]);
}
/**
* Construct a new issue galley.
*
* @return IssueGalley
*/
public function newDataObject()
{
return new IssueGalley();
}
/**
* Internal function to return an IssueGalley object from a row.
*
* @param array $row
*
* @return IssueGalley
*/
public function _fromRow($row)
{
$galley = $this->newDataObject();
$galley->setId($row['galley_id']);
$galley->setIssueId($row['issue_id']);
$galley->setLocale($row['locale']);
$galley->setFileId($row['file_id']);
$galley->setLabel($row['label']);
$galley->setSequence($row['seq']);
$galley->setData('urlPath', $row['url_path']);
// IssueFile set methods
$galley->setServerFileName($row['file_name']);
$galley->setOriginalFileName($row['original_file_name']);
$galley->setFileType($row['file_type']);
$galley->setFileSize($row['file_size']);
$galley->setContentType($row['content_type']);
$galley->setDateModified($this->datetimeFromDB($row['date_modified']));
$galley->setDateUploaded($this->datetimeFromDB($row['date_uploaded']));
$this->getDataObjectSettings('issue_galley_settings', 'galley_id', $row['galley_id'], $galley);
Hook::call('IssueGalleyDAO::_fromRow', [&$galley, &$row]);
return $galley;
}
/**
* Insert a new IssueGalley.
*
* @param IssueGalley $galley
*/
public function insertObject($galley)
{
$this->update(
'INSERT INTO issue_galleys
(issue_id,
file_id,
label,
locale,
seq,
url_path)
VALUES
(?, ?, ?, ?, ?, ?)',
[
(int) $galley->getIssueId(),
(int) $galley->getFileId(),
$galley->getLabel(),
$galley->getLocale(),
$galley->getSequence() == null ? $this->getNextGalleySequence($galley->getIssueId()) : $galley->getSequence(),
$galley->getData('urlPath'),
]
);
$galley->setId($this->getInsertId());
$this->updateLocaleFields($galley);
Hook::call('IssueGalleyDAO::insertObject', [&$galley, $galley->getId()]);
return $galley->getId();
}
/**
* Update an existing IssueGalley.
*
* @param IssueGalley $galley
*/
public function updateObject($galley)
{
$this->update(
'UPDATE issue_galleys
SET
file_id = ?,
label = ?,
locale = ?,
seq = ?,
url_path = ?
WHERE galley_id = ?',
[
(int) $galley->getFileId(),
$galley->getLabel(),
$galley->getLocale(),
$galley->getSequence(),
$galley->getData('urlPath'),
(int) $galley->getId()
]
);
$this->updateLocaleFields($galley);
}
/**
* Delete an IssueGalley.
*
* @param IssueGalley $galley
*/
public function deleteObject($galley)
{
return $this->deleteById($galley->getId(), $galley->getIssueId());
}
/**
* Delete a galley by ID.
*
* @param int $galleyId
* @param int $issueId optional
*/
public function deleteById($galleyId, $issueId = null)
{
Hook::call('IssueGalleyDAO::deleteById', [&$galleyId, &$issueId]);
if (isset($issueId)) {
// Delete the file
$issueGalley = $this->getById($galleyId);
$issueFileManager = new IssueFileManager($issueId);
$issueFileManager->deleteById($issueGalley->getFileId());
$affectedRows = $this->update(
'DELETE FROM issue_galleys WHERE galley_id = ? AND issue_id = ?',
[(int) $galleyId, (int) $issueId]
);
} else {
$affectedRows = $this->update(
'DELETE FROM issue_galleys WHERE galley_id = ?',
[(int) $galleyId]
);
}
if ($affectedRows) {
$this->update('DELETE FROM issue_galley_settings WHERE galley_id = ?', [(int) $galleyId]);
}
}
/**
* Delete galleys by issue.
* NOTE that this will not delete issue_file entities or the respective files.
*
* @param int $issueId
*/
public function deleteByIssueId($issueId)
{
$galleys = $this->getByIssueId($issueId);
foreach ($galleys as $galley) {
$this->deleteById($galley->getId(), $issueId);
}
}
/**
* Sequentially renumber galleys for an issue in their sequence order.
*
* @param int $issueId
*/
public function resequence($issueId)
{
$result = $this->retrieve('SELECT galley_id FROM issue_galleys WHERE issue_id = ? ORDER BY seq', [(int) $issueId]);
for ($i = 1; $row = $result->current(); $i++) {
$this->update('UPDATE issue_galleys SET seq = ? WHERE galley_id = ?', [$i, $row->galley_id]);
$result->next();
}
}
/**
* Get the the next sequence number for an issue's galleys (i.e., current max + 1).
*
* @param int $issueId
*
* @return int
*/
public function getNextGalleySequence($issueId)
{
$result = $this->retrieve('SELECT COALESCE(MAX(seq), 0) + 1 AS next_sequence FROM issue_galleys WHERE issue_id = ?', [(int) $issueId]);
$row = $result->current();
return $row->next_sequence;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\issue\IssueGalleyDAO', '\IssueGalleyDAO');
}
+323
View File
@@ -0,0 +1,323 @@
<?php
/**
* @file classes/issue/Repository.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage issues.
*/
namespace APP\issue;
use APP\core\Request;
use APP\facades\Repo;
use APP\file\IssueFileManager;
use APP\file\PublicFileManager;
use APP\journal\JournalDAO;
use Illuminate\Support\Collection;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\doi\exceptions\DoiException;
use PKP\plugins\Hook;
use PKP\services\PKPSchemaService;
use PKP\validation\ValidatorFactory;
class Repository
{
/** @var DAO $dao */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schema */
public $schemaMap = maps\Schema::class;
/** @var Request $request */
protected $request;
/** @var PKPSchemaService $schemaService */
protected $schemaService;
// TODO: Explicitly excluding caching for now as it wasn't actually setting data to cache
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->dao = $dao;
$this->request = $request;
$this->schemaService = $schemaService;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): Issue
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::exists() */
public function exists(int $id, ?int $journalId = null): bool
{
return $this->dao->exists($id, $journalId);
}
/** @copydoc DAO::get()
* TODO: Function signature should stick with ID, but previous DAO expected $useCache = false as default
*/
public function get(int $id, ?int $journalId = null): ?Issue
{
// TODO: Caching as currently setup never properly caches objects and always fires a _cacheMiss()
// if ($useCache) {
// $cache = $this->dao->_getCache('issues');
// $returner = $cache->get($id);
// if ($returner && $contextId != null && $contextId != $returner->getJournalId()) {
// $returner = null;
// }
// return $returner;
// }
return $this->dao->get($id, $journalId);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* announcements to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for an issue
*
* Perform validation checks on data used to add or edit an issue.
*
* @param array $props A key/value array with the new data to validate
* @param array $allowedLocales The context's supported locales
* @param string $primaryLocale The context's primary locale
*
* @throws \Exception
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(?Issue $object, array $props, array $allowedLocales, string $primaryLocale): array
{
$errors = [];
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules($this->dao->schema, $allowedLocales),
);
// Check required fields
ValidatorFactory::required(
$validator,
$object,
$this->schemaService->getRequiredProps($this->dao->schema),
$this->schemaService->getMultilingualProps($this->dao->schema),
$allowedLocales,
$primaryLocale
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Issue::validate', [&$errors, $object, $props, $allowedLocales, $primaryLocale]);
return $errors;
}
/** @copydoc DAO::insert() */
public function add(Issue $issue): int
{
return $this->dao->insert($issue);
}
/** @copydoc DAO::update() */
public function edit(Issue $issue, array $params)
{
$newIssue = $this->newDataObject(array_merge($issue->_data, $params));
Hook::call('Issue::edit', [&$newIssue, $issue, $params]);
$this->dao->update($newIssue);
}
/** @copydoc DAO::delete() */
public function delete(Issue $issue)
{
$publicFileManager = new PublicFileManager();
if (is_array($issue->getCoverImage(null))) {
foreach ($issue->getCoverImage(null) as $coverImage) {
if ($coverImage != '') {
$publicFileManager->removeContextFile($issue->getJournalId(), $coverImage);
}
}
}
$issueId = $issue->getId();
// Delete issue-specific ordering if it exists.
Repo::section()->deleteCustomSectionOrdering($issueId);
// Delete published issue galleys and issue files
$issueGalleyDao = DAORegistry::getDAO('IssueGalleyDAO'); /** @var IssueGalleyDAO $issueGalleyDao */
$issueGalleyDao->deleteByIssueId($issueId);
$issueFileDao = DAORegistry::getDAO('IssueFileDAO'); /** @var IssueFileDAO $issueFileDao */
$issueFileDao->deleteByIssueId($issueId);
$issueFileManager = new IssueFileManager($issueId);
$issueFileManager->deleteIssueTree();
$this->dao->deleteCustomIssueOrdering($issueId);
$this->dao->delete($issue);
}
public function deleteMany(Collector $collector)
{
foreach ($collector->getIds() as $issueId) {
$this->dao->deleteById($issueId);
}
}
/**
* Retrieve current issue
*
* @param bool $useCache TODO: Not currently implemented. Adding to preserved desired cache usage in future
*/
public function getCurrent(int $contextId, bool $useCache = false): ?Issue
{
// TODO: Caching as currently setup never properly caches objects and always fires a _cacheMiss()
// if ($useCache) {
// $cache = $this->dao->_getCache('current');
// return $cache->get($contextId);
// }
/** @var JournalDAO $journalDao */
$journalDao = DAORegistry::getDAO('JournalDAO');
$journal = $journalDao->getById($contextId);
$issueId = $journal->getData('currentIssueId');
return $issueId != null ? $this->get($issueId) : null;
}
/**
* Update the current issue for the journal.
*
*/
public function updateCurrent(int $contextId, ?Issue $newCurrentIssue = null)
{
/** @var JournalDAO $journalDao */
$journalDao = DAORegistry::getDAO('JournalDAO');
$journal = $journalDao->getById($contextId);
if ($newCurrentIssue) {
$journal->setData('currentIssueId', $newCurrentIssue->getId());
$this->edit($newCurrentIssue, []);
$journalDao->updateObject($journal);
} else {
$journalDao->removeCurrentIssue($journal->getId());
}
}
/**
* Get issue by a submission id
*
*/
public function getBySubmissionId(int $submissionId): ?Issue
{
$issueId = Repo::submission()
->get($submissionId)
?->getCurrentPublication()
?->getData('issueId');
return $issueId ? $this->get($issueId) : null;
}
/**
* Retrieve Issue by "best" issue id -- url path if it exists,
* falling back on the internal issue ID otherwise.
*
* @param bool $useCache TODO: Carryover from IssueDAO—was not in use
*/
public function getByBestId(string $idOrUrlPath, ?int $contextId = null, bool $useCache = false): ?Issue
{
// Get the issue that matches the requested urlPath (if $idOrUrlPath is one)
return ctype_digit((string) $idOrUrlPath)
? $this->get((int) $idOrUrlPath, $contextId)
: $this->getByUrlPath($idOrUrlPath, $contextId);
}
/**
* Get an issue by its urlPath
*
*/
public function getByUrlPath(string $urlPath, int $contextId): ?Issue
{
$issueId = $this->dao->getIdByUrlPath($urlPath, $contextId);
return $issueId ? $this->get($issueId) : null;
}
/**
* Gets a list of all the years in which issues have been published
*
*/
public function getYearsIssuesPublished(int $contextId): Collection
{
return $this->dao->getYearsIssuesPublished($contextId);
}
/**
* Deletes all Issues for a given context
*
*/
public function deleteByContextId(int $contextId)
{
$collector = $this->getCollector()->filterByContextIds([$contextId]);
$this->deleteMany($collector);
}
/**
* Creates a DOI for the given issue
*
* @return DoiException[]
*/
public function createDoi(Issue $issue): array
{
/** @var JournalDAO $contextDao */
$contextDao = DAORegistry::getDAO('JournalDAO');
$context = $contextDao->getById($issue->getData('journalId'));
$doiCreationFailures = [];
if ($context->isDoiTypeEnabled(Repo::doi()::TYPE_ISSUE) && empty($issue->getData('doiId'))) {
try {
$doiId = Repo::doi()->mintIssueDoi($issue, $context);
$issue->setData('doiId', $doiId);
$this->dao->update($issue);
} catch (DoiException $exception) {
$doiCreationFailures[] = $exception;
}
}
return $doiCreationFailures;
}
}
+218
View File
@@ -0,0 +1,218 @@
<?php
/**
* @file classes/issue/maps/Schema.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2003-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map sections to the properties defined in the issue schema
*/
namespace APP\issue\maps;
use APP\core\Application;
use APP\facades\Repo;
use APP\issue\Issue;
use APP\issue\IssueGalleyDAO;
use APP\journal\Journal;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\db\DAORegistry;
use PKP\services\PKPSchemaService;
use PKP\submission\Genre;
use PKP\userGroup\UserGroup;
class Schema extends \PKP\core\maps\Schema
{
/** @copydoc \PKP\core\maps\Schema::$collection */
public Enumerable $collection;
/** @copydoc \PKP\core\maps\Schema::$schema */
public string $schema = PKPSchemaService::SCHEMA_ISSUE;
/** @var LazyCollection<int,UserGroup> The user groups for this context. */
public LazyCollection $userGroups;
/** @var Genre[] The genres for this context. */
public array $genres;
/**
* Map an Issue
*
* Includes all properties in the Issue schema
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups of this content
* @param Genre[] $genres The genres of this context
*/
public function map(Issue $item, Journal $context, LazyCollection $userGroups, array $genres): array
{
$this->userGroups = $userGroups;
$this->genres = $genres;
return $this->mapByProperties($this->getProps(), $item);
}
/**
* Summarize an Issue
*
* Includes properties with the apiSummary flag in the Issue schema.
*
*/
public function summarize(Issue $item, Journal $context): array
{
$this->context = $context;
return $this->mapByProperties($this->getSummaryProps(), $item);
}
/**
* Map a collection of Issues
*
* @see self::map
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups of this content
* @param Genre[] $genres The genres of this context
*/
public function mapMany(Enumerable $collection, Journal $context, LazyCollection $userGroups, array $genres): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) use ($context, $userGroups, $genres) {
return $this->map($item, $context, $userGroups, $genres);
});
}
/**
* Summarize a collection of Issues
*
* @see self::summarize
*/
public function summarizeMany(Enumerable $collection, Journal $context): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) use ($context) {
return $this->summarize($item, $context);
});
}
/**
* Map schema properties of an Issue to an assoc array
*
*/
private function mapByProperties(array $props, Issue $issue): array
{
$output = [];
foreach ($props as $prop) {
switch ($prop) {
case '_href':
$output[$prop] = $this->getApiUrl('issues/' . $issue->getId(), $this->context->getData('urlPath'));
break;
case 'articles':
$data = [];
$submissions = Repo::submission()
->getCollector()
->filterByContextIds([$issue->getJournalId()])
->filterByIssueIds([$issue->getId()])
->getMany();
foreach ($submissions as $submission) {
$data[] = Repo::submission()->getSchemaMap()->summarize($submission, $this->userGroups, $this->genres);
}
$output[$prop] = $data;
break;
case 'coverImageUrl':
$output[$prop] = $issue->getCoverImageUrls();
break;
case 'doiObject':
if ($issue->getData('doiObject')) {
$retVal = Repo::doi()->getSchemaMap()->summarize($issue->getData('doiObject'));
} else {
$retVal = null;
}
$output[$prop] = $retVal;
break;
case 'galleys':
$data = [];
/** @var IssueGalleyDAO $issueGalleyDao */
$issueGalleyDao = DAORegistry::getDAO('IssueGalleyDAO');
$galleys = $issueGalleyDao->getByIssueId($issue->getId());
if (!empty($galleys)) {
$request = Application::get()->getRequest();
foreach ($galleys as $galley) {
$data[] = [
'fileId' => $galley->getData('fileId'),
'label' => $galley->getData('label'),
'locale' => $galley->getData('locale'),
'pub-id::publisher-id' => $galley->getData('pub-id::publisher-id'),
'sequence' => $galley->getData('sequence'),
'urlPublished' => $request->getDispatcher()->url(
$request,
Application::ROUTE_PAGE,
$this->context->getPath(),
'issue',
'view',
[
$galley->getIssueId(),
$galley->getId()
]
),
'urlRemote' => $galley->getData('urlRemote'),
];
}
}
$output[$prop] = $data;
break;
case 'publishedUrl':
$output['publishedUrl'] = $this->request->getDispatcher()->url(
$this->request,
Application::ROUTE_PAGE,
$this->context->getPath(),
'issue',
'view',
$issue->getId()
);
break;
case 'sections':
$data = [];
$sections = Repo::section()->getByIssueId($issue->getId());
if (!empty($sections)) {
$seq = 1;
foreach ($sections as $section) {
$sectionProperties = Repo::section()->getSchemaMap()->summarize($section);
$sectionProperties['seq'] = $seq;
$seq++;
$data[] = $sectionProperties;
}
}
$output[$prop] = $data;
break;
case 'identification':
$output[$prop] = $issue->getIssueIdentification();
break;
default:
$output[$prop] = $issue->getData($prop);
}
}
return $output;
}
/**
* Map an issue with only the issue identification for the stats list
*/
public function mapToStats(Issue $issue): array
{
$props = $this->mapByProperties([
'_href',
'id',
'identification',
'publishedUrl'
], $issue);
return $props;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
/**
* @defgroup journal Journal
* Extensions to the pkp-lib "context" concept to specialize it for use in OJS
* in representing Journal objects and journal-specific concerns.
*/
/**
* @file classes/journal/Journal.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Journal
*
* @ingroup journal
*
* @see JournalDAO
*
* @brief Describes basic journal properties.
*/
namespace APP\journal;
use APP\core\Application;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
class Journal extends Context
{
public const PUBLISHING_MODE_OPEN = 0;
public const PUBLISHING_MODE_SUBSCRIPTION = 1;
public const PUBLISHING_MODE_NONE = 2;
/**
* Get "localized" journal page title (if applicable).
*
* @return string|null
*
* @deprecated 3.3.0, use getLocalizedData() instead
*/
public function getLocalizedPageHeaderTitle()
{
$titleArray = $this->getData('name');
foreach ([Locale::getLocale(), Locale::getPrimaryLocale()] as $locale) {
if (isset($titleArray[$locale])) {
return $titleArray[$locale];
}
}
return null;
}
/**
* Get "localized" journal page logo (if applicable).
*
* @return array|null
*
* @deprecated 3.3.0, use getLocalizedData() instead
*/
public function getLocalizedPageHeaderLogo()
{
$logoArray = $this->getData('pageHeaderLogoImage');
foreach ([Locale::getLocale(), Locale::getPrimaryLocale()] as $locale) {
if (isset($logoArray[$locale])) {
return $logoArray[$locale];
}
}
return null;
}
//
// Get/set methods
//
/**
* Get the association type for this context.
*
* @return int
*/
public function getAssocType()
{
return Application::ASSOC_TYPE_JOURNAL;
}
/**
* @copydoc \PKP\core\DataObject::getDAO()
*
* @return JournalDAO
*/
public function getDAO()
{
return DAORegistry::getDAO('JournalDAO');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\journal\Journal', '\Journal');
foreach ([
'PUBLISHING_MODE_OPEN',
'PUBLISHING_MODE_SUBSCRIPTION',
'PUBLISHING_MODE_NONE',
] as $constantName) {
define($constantName, constant('\Journal::' . $constantName));
}
}
+166
View File
@@ -0,0 +1,166 @@
<?php
/**
* @file classes/journal/JournalDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class JournalDAO
*
* @ingroup journal
*
* @see Journal
*
* @brief Operations for retrieving and modifying Journal objects.
*/
namespace APP\journal;
use APP\core\Application;
use APP\facades\Repo;
use PKP\context\ContextDAO;
use PKP\db\DAORegistry;
use PKP\metadata\MetadataTypeDescription;
/**
* @extends ContextDAO<Journal>
*/
class JournalDAO extends ContextDAO
{
/** @copydoc SchemaDAO::$schemaName */
public $schemaName = 'context';
/** @copydoc SchemaDAO::$tableName */
public $tableName = 'journals';
/** @copydoc SchemaDAO::$settingsTableName */
public $settingsTableName = 'journal_settings';
/** @copydoc SchemaDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'journal_id';
/** @var array Maps schema properties for the primary table to their column names */
public $primaryTableColumns = [
'id' => 'journal_id',
'urlPath' => 'path',
'enabled' => 'enabled',
'seq' => 'seq',
'primaryLocale' => 'primary_locale',
'currentIssueId' => 'current_issue_id'
];
/**
* Create a new DataObject of the appropriate class
*
* @return Journal
*/
public function newDataObject()
{
return new Journal();
}
/**
* Retrieve the IDs and titles of all journals in an associative array.
*
* @return array<int,string>
*/
public function getTitles($enabledOnly = false)
{
$journals = [];
$journalIterator = $this->getAll($enabledOnly);
while ($journal = $journalIterator->next()) {
$journals[$journal->getId()] = $journal->getLocalizedName();
}
return $journals;
}
/**
* Delete the public IDs of all publishing objects in a journal.
*
* @param int $journalId
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
*/
public function deleteAllPubIds($journalId, $pubIdType)
{
Repo::galley()->dao->deleteAllPubIds($journalId, $pubIdType);
Repo::submissionFile()->dao->deleteAllPubIds($journalId, $pubIdType);
Repo::issue()->dao->deleteAllPubIds($journalId, $pubIdType);
Repo::publication()->dao->deleteAllPubIds($journalId, $pubIdType);
}
/**
* Check whether the given public ID exists for any publishing
* object in a journal.
*
* @param int $journalId
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
* @param int $assocType The object type of an object to be excluded from
* the search. Identified by one of the Application::ASSOC_TYPE_* constants.
* @param int $assocId The id of an object to be excluded from the search.
* @param bool $forSameType Whether only the same objects should be considered.
*
* @return bool
*/
public function anyPubIdExists(
$journalId,
$pubIdType,
$pubId,
$assocType = MetadataTypeDescription::ASSOC_TYPE_ANY,
$assocId = 0,
$forSameType = false
) {
$pubObjectDaos = [
Application::ASSOC_TYPE_ISSUE => Repo::issue()->dao,
Application::ASSOC_TYPE_PUBLICATION => Repo::publication()->dao,
Application::ASSOC_TYPE_GALLEY => Application::getRepresentationDAO(),
Application::ASSOC_TYPE_ISSUE_GALLEY => DAORegistry::getDAO('IssueGalleyDAO'),
Application::ASSOC_TYPE_SUBMISSION_FILE => Repo::submissionFile()->dao,
];
if ($forSameType) {
$dao = $pubObjectDaos[$assocType];
$excludedId = $assocId;
if ($dao->pubIdExists($pubIdType, $pubId, $excludedId, $journalId)) {
return true;
}
return false;
}
foreach ($pubObjectDaos as $daoAssocType => $dao) {
if ($assocType == $daoAssocType) {
$excludedId = $assocId;
} else {
$excludedId = 0;
}
if ($dao->pubIdExists($pubIdType, $pubId, $excludedId, $journalId)) {
return true;
}
}
return false;
}
/**
* Sets current_issue_id for context to null.
* This is necessary because current_issue_id should explicitly be set to null rather than unset.
*
* @param int $contextId
*
* @return int
*/
public function removeCurrentIssue($contextId)
{
return $this->update(
"UPDATE {$this->tableName} SET current_issue_id = null WHERE {$this->primaryKeyColumn} = ?",
[(int) $contextId]
);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\journal\JournalDAO', '\JournalDAO');
}
@@ -0,0 +1,78 @@
<?php
/**
* @file classes/log/event/SubmissionEventLogEntry.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionEventLogEntry
*
* @ingroup log
*
* @brief Describes an entry in the submission history log.
*/
namespace APP\log\event;
use PKP\log\event\PKPSubmissionEventLogEntry;
// Log entry associative types. All types must be defined here
// General events 0x10000000
define('SUBMISSION_LOG_SUPPFILE_UPDATE', 0x10000003);
define('SUBMISSION_LOG_ISSUE_SCHEDULE', 0x10000004);
define('SUBMISSION_LOG_ISSUE_ASSIGN', 0x10000005);
define('SUBMISSION_LOG_ARTICLE_PUBLISH', 0x10000006);
define('SUBMISSION_LOG_ARTICLE_IMPORT', 0x10000007);
define('SUBMISSION_LOG_ISSUE_METADATA_UPDATE', 0x10000008);
// Author events 0x20000000
define('SUBMISSION_LOG_AUTHOR_REVISION', 0x20000001);
// Editor events 0x30000000
define('SUBMISSION_LOG_EDITOR_ASSIGN', 0x30000001);
define('SUBMISSION_LOG_EDITOR_UNASSIGN', 0x30000002);
define('SUBMISSION_LOG_EDITOR_FILE', 0x30000004);
define('SUBMISSION_LOG_EDITOR_ARCHIVE', 0x30000005);
define('SUBMISSION_LOG_EDITOR_RESTORE', 0x30000006);
// Reviewer events 0x40000000
define('SUBMISSION_LOG_REVIEW_RECOMMENDATION_BY_PROXY', 0x40000016);
// Copyeditor events 0x50000000
define('SUBMISSION_LOG_COPYEDIT_ASSIGN', 0x50000001);
define('SUBMISSION_LOG_COPYEDIT_UNASSIGN', 0x50000002);
define('SUBMISSION_LOG_COPYEDIT_INITIATE', 0x50000003);
define('SUBMISSION_LOG_COPYEDIT_REVISION', 0x50000004);
define('SUBMISSION_LOG_COPYEDIT_INITIAL', 0x50000005);
define('SUBMISSION_LOG_COPYEDIT_FINAL', 0x50000006);
define('SUBMISSION_LOG_COPYEDIT_SET_FILE', 0x50000007);
define('SUBMISSION_LOG_COPYEDIT_COPYEDIT_FILE', 0x50000008);
define('SUBMISSION_LOG_COPYEDIT_COPYEDITOR_FILE', 0x50000009);
// Proofreader events 0x60000000
define('SUBMISSION_LOG_PROOFREAD_ASSIGN', 0x60000001);
define('SUBMISSION_LOG_PROOFREAD_UNASSIGN', 0x60000002);
define('SUBMISSION_LOG_PROOFREAD_INITIATE', 0x60000003);
define('SUBMISSION_LOG_PROOFREAD_REVISION', 0x60000004);
define('SUBMISSION_LOG_PROOFREAD_COMPLETE', 0x60000005);
// Layout events 0x70000000
define('SUBMISSION_LOG_LAYOUT_ASSIGN', 0x70000001);
define('SUBMISSION_LOG_LAYOUT_UNASSIGN', 0x70000002);
define('SUBMISSION_LOG_LAYOUT_INITIATE', 0x70000003);
define('SUBMISSION_LOG_LAYOUT_GALLEY', 0x70000004);
define('SUBMISSION_LOG_LAYOUT_COMPLETE', 0x70000005);
define('SUBMISSION_LOG_LAYOUT_GALLEY_AVAILABLE', 0x70000006);
define('SUBMISSION_LOG_LAYOUT_GALLEY_UNAVAILABLE', 0x70000007);
class SubmissionEventLogEntry extends PKPSubmissionEventLogEntry
{
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\log\event\SubmissionEventLogEntry', '\SubmissionEventLogEntry');
}
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* @file classes/mailable/Repository.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and edit Mailables.
*/
namespace APP\mail;
use Illuminate\Support\Collection;
class Repository extends \PKP\mail\Repository
{
/**
* Registers app-specific mailables
*/
public function map(): Collection
{
return parent::map()->merge(collect([
mailables\IssuePublishedNotify::class,
mailables\OpenAccessNotify::class,
mailables\SubscriptionExpired::class,
mailables\SubscriptionExpiredLast::class,
mailables\SubscriptionExpiresSoon::class,
mailables\SubscriptionNotify::class,
mailables\SubscriptionPurchaseIndividual::class,
mailables\SubscriptionPurchaseInstitutional::class,
mailables\SubscriptionRenewIndividual::class,
mailables\SubscriptionRenewInstitutional::class,
mailables\PaymentRequest::class,
]));
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @file classes/mail/mailables/IssuePublishedNotify.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssuePublishedNotify
*
* @ingroup mail_mailables
*
* @brief Email is sent automatically to all registered users to notify about new published issue
*/
namespace APP\mail\mailables;
use APP\issue\Issue;
use APP\mail\variables\IssueEmailVariable;
use PKP\context\Context;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\mail\traits\Sender;
use PKP\mail\traits\Unsubscribe;
use PKP\security\Role;
class IssuePublishedNotify extends Mailable
{
use Configurable;
use Recipient;
use Sender;
use Unsubscribe;
protected static ?string $name = 'mailable.issuePublishNotify.name';
protected static ?string $description = 'mailable.issuePublishNotify.description';
protected static ?string $emailTemplateKey = 'ISSUE_PUBLISH_NOTIFY';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_SUB_EDITOR];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
protected Context $context;
public function __construct(Context $context, Issue $issue)
{
parent::__construct([$context, $issue]);
$this->context = $context;
}
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Issue::class] = IssueEmailVariable::class;
return $map;
}
/**
* Adds a footer with unsubscribe link
*/
protected function addFooter(string $locale): Mailable
{
$this->setupUnsubscribeFooter($locale, $this->context);
return $this;
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @file classes/mail/mailables/OpenAccessNotify.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class OpenAccessNotify
*
* @brief Email sent to notify user about issue open access
*/
namespace APP\mail\mailables;
use APP\issue\Issue;
use APP\journal\Journal;
use APP\mail\variables\IssueEmailVariable;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\mail\traits\Unsubscribe;
use PKP\security\Role;
class OpenAccessNotify extends Mailable
{
use Configurable;
use Recipient;
use Unsubscribe;
protected static ?string $name = 'mailable.openAccessNotify.name';
protected static ?string $description = 'mailable.openAccessNotify.description';
protected static ?string $emailTemplateKey = 'OPEN_ACCESS_NOTIFY';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [self::FROM_SYSTEM];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
protected Journal $context;
public function __construct(Journal $context, Issue $issue)
{
parent::__construct([$context, $issue]);
$this->context = $context;
}
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Issue::class] = IssueEmailVariable::class;
return $map;
}
/**
* Adds a footer with unsubscribe link
*/
protected function addFooter(string $locale): Mailable
{
$this->setupUnsubscribeFooter($locale, $this->context);
return $this;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
/**
* @file classes/mail/mailables/PaymentRequest.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PaymentRequest
*
* @ingroup mail_mailables
*
* @brief Email is sent automatically to notify authors of the submission about the required payment
*/
namespace APP\mail\mailables;
use APP\core\Application;
use APP\journal\Journal;
use APP\mail\variables\ContextEmailVariable;
use APP\submission\Submission;
use PKP\core\PKPApplication;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\mail\variables\ContextEmailVariable as PKPContextEmailVariable;
use PKP\payment\QueuedPayment;
use PKP\security\Role;
class PaymentRequest extends Mailable
{
use Configurable;
use Recipient;
protected static ?string $name = 'mailable.paymentRequest.name';
protected static ?string $description = 'mailable.paymentRequest.description';
protected static ?string $emailTemplateKey = 'PAYMENT_REQUEST_NOTIFICATION';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [self::FROM_SYSTEM];
protected static array $toRoleIds = [Role::ROLE_ID_AUTHOR];
protected static string $queuedPaymentUrl = 'queuedPaymentUrl';
protected static string $submissionGuidelinesUrl = 'submissionGuidelinesUrl';
public function __construct(Journal $context, Submission $submission, QueuedPayment $queuedPayment)
{
parent::__construct(func_get_args());
$this->setupPaymentUrlVariable($context, $queuedPayment);
}
protected function setupPaymentUrlVariable(Journal $context, QueuedPayment $queuedPayment)
{
$request = Application::get()->getRequest();
$dispatcher = $request->getDispatcher();
$this->addData([
static::$queuedPaymentUrl => $dispatcher->url(
$request,
PKPApplication::ROUTE_PAGE,
$context->getPath(),
'payment',
'pay',
[$queuedPayment->getId()]
),
static::$submissionGuidelinesUrl => $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$context->getPath(),
'about',
'submissions'
),
]);
}
public static function getDataDescriptions(): array
{
return array_merge(
parent::getDataDescriptions(),
[
static::$queuedPaymentUrl => __('emailTemplate.variable.queuedPaymentUrl'),
static::$submissionGuidelinesUrl => __('emailTemplate.variable.submissionGuidelinesUrl'),
]
);
}
protected function addFooter(string $locale): self
{
$this->footer = $this->renameContextVariables(
__('emails.paymentRequestNotification.footer', [], $locale)
);
return $this;
}
/**
* Replace email template variables in the locale string, so they correspond to the application,
* e.g., contextName => journalName/pressName/serverName
*/
protected function renameContextVariables(string $footer): string
{
$map = [
'{$' . PKPContextEmailVariable::CONTEXT_NAME . '}' => '{$' . ContextEmailVariable::CONTEXT_NAME . '}',
'{$' . PKPContextEmailVariable::CONTEXT_URL . '}' => '{$' . ContextEmailVariable::CONTEXT_URL . '}',
];
return str_replace(array_keys($map), array_values($map), $footer);
}
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionExpired.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionExpired
*
* @brief Email sent automatically to notify a subscriber that their subscription has expired
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\security\Role;
class SubscriptionExpired extends Mailable
{
use Configurable;
use Recipient;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionExpired.name';
protected static ?string $description = 'mailable.subscriptionExpired.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_AFTER_EXPIRY';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [self::FROM_SYSTEM];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
public function __construct(Journal $context, Subscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionExpired.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionExpired
*
* @brief Email sent automatically to notify a subscriber the second time that their subscription has expired
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\security\Role;
class SubscriptionExpiredLast extends Mailable
{
use Configurable;
use Recipient;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionExpiredLast.name';
protected static ?string $description = 'mailable.subscriptionExpiredLast.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_AFTER_EXPIRY_LAST';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [self::FROM_SYSTEM];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
public function __construct(Journal $context, Subscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionExpiresSoon.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionExpiresSoon
*
* @brief Email sent automatically to notify a subscriber that their subscription expires soon
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\security\Role;
class SubscriptionExpiresSoon extends Mailable
{
use Configurable;
use Recipient;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionExpiresSoon.name';
protected static ?string $description = 'mailable.subscriptionExpiresSoon.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_BEFORE_EXPIRY';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [self::FROM_SYSTEM];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
public function __construct(Journal $context, Subscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionNotify.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionNotify
*
* @brief Email sent to notify user about new subscription
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Recipient;
use PKP\security\Role;
class SubscriptionNotify extends Mailable
{
use Configurable;
use Recipient;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionNotify.name';
protected static ?string $description = 'mailable.subscriptionNotify.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_NOTIFY';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_SUBSCRIPTION_MANAGER];
protected static array $toRoleIds = [Role::ROLE_ID_READER];
public function __construct(Journal $context, Subscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionPurchaseIndividual.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionPurchaseIndividual
*
* @brief Email sent automatically to notify a subscription manager about new subscription
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\IndividualSubscription;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Sender;
use PKP\security\Role;
class SubscriptionPurchaseIndividual extends Mailable
{
use Configurable;
use Sender;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionPurchaseIndividual.name';
protected static ?string $description = 'mailable.subscriptionPurchaseIndividual.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_PURCHASE_INDL';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_READER];
protected static array $toRoleIds = [Role::ROLE_ID_SUBSCRIPTION_MANAGER];
public function __construct(Journal $context, IndividualSubscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionPurchaseInstitutional.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionPurchaseInstitutional
*
* @brief Email sent automatically to notify a subscription manager about new subscription
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionInstitutional;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\InstitutionalSubscription;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\institution\Institution;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Sender;
use PKP\security\Role;
class SubscriptionPurchaseInstitutional extends Mailable
{
use Configurable;
use Sender;
use SubscriptionInstitutional;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionPurchaseInstitutional.name';
protected static ?string $description = 'mailable.subscriptionPurchaseInstitutional.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_PURCHASE_INSTL';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_READER];
protected static array $toRoleIds = [Role::ROLE_ID_SUBSCRIPTION_MANAGER];
public function __construct(
Journal $context,
InstitutionalSubscription $subscription,
SubscriptionType $subscriptionType,
Institution $institution
) {
parent::__construct([$context, $subscription]);
$this->setupInstitutionalVariables($subscription, $institution);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for institution related template variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
$variables = static::addInstitutionalVariablesDescription($variables);
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionRenewIndividual.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionRenewIndividual
*
* @brief Email sent automatically to notify a subscription manager about subscription renewal
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\IndividualSubscription;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Sender;
use PKP\security\Role;
class SubscriptionRenewIndividual extends Mailable
{
use Configurable;
use Sender;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionRenewIndividual.name';
protected static ?string $description = 'mailable.subscriptionRenewIndividual.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_RENEW_INDL';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_READER];
protected static array $toRoleIds = [Role::ROLE_ID_SUBSCRIPTION_MANAGER];
public function __construct(Journal $context, IndividualSubscription $subscription, SubscriptionType $subscriptionType)
{
parent::__construct([$context, $subscription]);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for subscription type related variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @file classes/mail/mailables/SubscriptionRenewInstitutional.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionRenewInstitutional
*
* @brief Email sent automatically to notify a subscription manager about subscription renewal
*/
namespace APP\mail\mailables;
use APP\journal\Journal;
use APP\mail\traits\SubscriptionInstitutional;
use APP\mail\traits\SubscriptionTypeVariables;
use APP\mail\variables\SubscriptionEmailVariable;
use APP\subscription\InstitutionalSubscription;
use APP\subscription\Subscription;
use APP\subscription\SubscriptionType;
use PKP\institution\Institution;
use PKP\mail\Mailable;
use PKP\mail\traits\Configurable;
use PKP\mail\traits\Sender;
use PKP\security\Role;
class SubscriptionRenewInstitutional extends Mailable
{
use Configurable;
use Sender;
use SubscriptionInstitutional;
use SubscriptionTypeVariables;
protected static ?string $name = 'mailable.subscriptionRenewInstitutional.name';
protected static ?string $description = 'mailable.subscriptionRenewInstitutional.description';
protected static ?string $emailTemplateKey = 'SUBSCRIPTION_RENEW_INSTL';
protected static array $groupIds = [self::GROUP_OTHER];
protected static array $fromRoleIds = [Role::ROLE_ID_READER];
protected static array $toRoleIds = [Role::ROLE_ID_SUBSCRIPTION_MANAGER];
public function __construct(
Journal $context,
InstitutionalSubscription $subscription,
SubscriptionType $subscriptionType,
Institution $institution,
) {
parent::__construct(func_get_args());
$this->setupInstitutionalVariables($subscription, $institution);
$this->setupSubscriptionTypeVariables($subscriptionType, $context);
}
/**
* Setup subscription related variables
*/
protected static function templateVariablesMap(): array
{
$map = parent::templateVariablesMap();
$map[Subscription::class] = SubscriptionEmailVariable::class;
return $map;
}
/**
* Description for institution related template variables
*/
public static function getDataDescriptions(): array
{
$variables = parent::getDataDescriptions();
$variables = static::addInstitutionalVariablesDescription($variables);
return static::addSubscriptionTypeVariablesDescription($variables);
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @file mail/traits/SubscriptionInstitutional.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionInstitutional
*
* @ingroup mail_traits
*
* @brief Mailable trait to set institutional subscription variables
*/
namespace APP\mail\traits;
use APP\subscription\InstitutionalSubscription;
use PKP\institution\Institution;
trait SubscriptionInstitutional
{
abstract public function addData(array $variables);
protected static string $institutionName = 'institutionName';
protected static string $institutionMailingAddress = 'institutionMailingAddress';
protected static string $domain = 'domain';
protected static string $ipRanges = 'ipRanges';
protected function setupInstitutionalVariables(InstitutionalSubscription $subscription, Institution $institution): void
{
$this->addData([
static::$institutionName => $institution->getLocalizedName(),
static::$institutionMailingAddress => $subscription->getInstitutionMailingAddress(),
static::$domain => $subscription->getDomain(),
static::$ipRanges => implode(' ', $institution->getIPRanges()),
]);
}
protected static function addInstitutionalVariablesDescription(array $variables): array
{
return array_merge(
$variables,
[
static::$institutionName => __('emailTemplate.variable.subscription.institutionName'),
static::$institutionMailingAddress => __('emailTemplate.variable.subscription.institutionMailingAddress'),
static::$domain => __('emailTemplate.variable.subscription.domain'),
static::$ipRanges => __('emailTemplate.variable.subscription.ipRanges'),
]
);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @file mail/traits/SubscriptionTypeVariables.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionTypeVariables
*
* @ingroup mail_traits
*
* @brief Mailable trait to set variables related to subscription type
*/
namespace APP\mail\traits;
use APP\core\Application;
use APP\journal\Journal;
use APP\subscription\SubscriptionType;
trait SubscriptionTypeVariables
{
protected static string $subscriptionUrl = 'subscriptionUrl';
protected static string $subscriptionType = 'subscriptionType';
abstract public function addData(array $variables);
protected function setupSubscriptionTypeVariables(SubscriptionType $subscriptionType, Journal $context): void
{
$this->addData([
static::$subscriptionUrl => $this->getSubscriptionUrl($subscriptionType, $context),
static::$subscriptionType => $subscriptionType->getSummaryString(),
]);
}
protected static function addSubscriptionTypeVariablesDescription(array $variables): array
{
return array_merge(
$variables,
[
static::$subscriptionUrl => __('emailTemplate.variable.subscription.subscriptionUrl'),
static::$subscriptionType => __('emailTemplate.variable.subscription.subscriptionType'),
]
);
}
protected function getSubscriptionUrl(SubscriptionType $subscriptionType, Journal $context): string
{
$application = Application::get();
$request = $application->getRequest();
$dispatcher = $application->getDispatcher();
return $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$context->getData('urlPath'),
'payments',
null,
null,
null,
$subscriptionType->getInstitutional() ? 'institutional' : 'individual',
);
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @file classes/mail/variables/ContextEmailVariable.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ContextEmailVariable
*
* @ingroup mail_variables
*
* @brief Represents journal-specific email template variables
*/
namespace APP\mail\variables;
use PKP\mail\variables\ContextEmailVariable as PKPContextEmailVariable;
class ContextEmailVariable extends PKPContextEmailVariable
{
public const CONTEXT_NAME = 'journalName';
public const CONTEXT_URL = 'journalUrl';
public const CONTEXT_SIGNATURE = 'journalSignature';
public const CONTEXT_ACRONYM = 'journalAcronym';
/**
* @copydoc Variable::descriptions()
*/
public static function descriptions(): array
{
return array_merge(
parent::descriptions(),
[
static::CONTEXT_ACRONYM => __('emailTemplate.variable.context.contextAcronym'),
]
);
}
/**
* @copydoc Variable::values()
*/
public function values(string $locale): array
{
$values = array_merge(
parent::values($locale),
[
static::CONTEXT_ACRONYM => htmlspecialchars($this->context->getLocalizedData('acronym')),
],
);
// Pass the values into the context signature so variables
// used in the signature can be rendered.
$values[static::CONTEXT_SIGNATURE] = $this->getContextSignature($values);
return $values;
}
}
@@ -0,0 +1,70 @@
<?php
/**
* @file classes/mail/variables/IssueEmailVariable.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class IssueEmailVariable
*
* @ingroup mail_variables
*
* @brief Email template variables for an issue.
*/
namespace APP\mail\variables;
use APP\core\Application;
use APP\issue\Issue;
use PKP\mail\Mailable;
use PKP\mail\variables\Variable;
class IssueEmailVariable extends Variable
{
public const ISSUE_ID = 'issueId';
public const ISSUE_IDENTIFICATION = 'issueIdentification';
public const ISSUE_URL = 'issueUrl';
protected Issue $issue;
public function __construct(Issue $issue, Mailable $mailable)
{
parent::__construct($mailable);
$this->issue = $issue;
}
public static function descriptions(): array
{
return
[
static::ISSUE_ID => __('emailTemplate.variable.issueId'),
static::ISSUE_IDENTIFICATION => __('emailTemplate.variable.issue.issueIdentification'),
static::ISSUE_URL => __('emailTemplate.variable.issue.issuePublishedUrl'),
];
}
public function values(string $locale): array
{
return
[
static::ISSUE_ID => $this->issue->getId(),
static::ISSUE_IDENTIFICATION => htmlspecialchars($this->issue->getIssueIdentification()),
static::ISSUE_URL => $this->getIssueUrl(),
];
}
protected function getIssueUrl(): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$this->getContext()->getPath(),
'issue',
'view',
$this->issue->getBestIssueId()
);
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @file classes/mail/variables/SubmissionEmailVariable.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionEmailVariable
*
* @ingroup mail_variables
*
* @brief Represents variables associated with a submission that can be assigned to a template
*/
namespace APP\mail\variables;
use APP\core\Application;
use PKP\context\Context;
class SubmissionEmailVariable extends \PKP\mail\variables\SubmissionEmailVariable
{
protected function getSubmissionPublishedUrl(Context $context): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$context->getPath(),
'article',
'view',
$this->submission->getBestId()
);
}
}
@@ -0,0 +1,95 @@
<?php
/**
* @file classes/mail/variables/SubscriptionEmailVariable.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubscriptionEmailVariable
*
* @ingroup mail_variables
*
* @brief Represents variables associated with a subscription
*/
namespace APP\mail\variables;
use APP\facades\Repo;
use APP\journal\Journal;
use APP\subscription\Subscription;
use PKP\core\PKPString;
use PKP\mail\Mailable;
use PKP\mail\variables\Variable;
use PKP\user\User;
class SubscriptionEmailVariable extends Variable
{
public const SUBSCRIBER_DETAILS = 'subscriberDetails';
public const SUBSCRIPTION_SIGNATURE = 'subscriptionSignature';
public const EXPIRY_DATE = 'expiryDate';
public const MEMBERSHIP = 'membership';
protected User $subscriber;
protected Subscription $subscription;
public function __construct(Subscription $subscription, Mailable $mailable)
{
parent::__construct($mailable);
$this->subscriber = Repo::user()->get($subscription->getUserId());
$this->subscription = $subscription;
}
/**
* @copydoc Variable::descriptions()
*/
public static function descriptions(): array
{
return
[
self::SUBSCRIBER_DETAILS => __('emailTemplate.variable.subscription.subscriberDetails'),
self::SUBSCRIPTION_SIGNATURE => __('emailTemplate.variable.subscription.subscriptionSignature'),
self::EXPIRY_DATE => __('emailTemplate.variable.subscription.expiryDate'),
self::MEMBERSHIP => __('emailTemplate.variable.subscription.membership'),
];
}
/**
* @copydoc Variable::values()
*/
public function values(string $locale): array
{
$context = $this->getContext();
return
[
self::SUBSCRIBER_DETAILS => PKPString::stripUnsafeHtml($this->subscriber->getSignature($locale) ?? ''),
self::SUBSCRIPTION_SIGNATURE => $this->getSubscriptionSignature($context),
self::EXPIRY_DATE => $this->subscription->getDateEnd(),
self::MEMBERSHIP => htmlspecialchars($this->subscription->getMembership()),
];
}
/**
* Subscription signature consisting of contact details of the person responsible for subscriptions included in the
* context's Subscription Policies form, Subscription Manager section
*/
protected function getSubscriptionSignature(Journal $context): string
{
$subscriptionName = htmlspecialchars($context->getData('subscriptionName'));
$subscriptionEmail = htmlspecialchars($context->getData('subscriptionEmail'));
$subscriptionPhone = htmlspecialchars($context->getData('subscriptionPhone'));
$subscriptionMailingAddress = PKPString::stripUnsafeHtml($context->getData('subscriptionMailingAddress'));
$subscriptionContactSignature = $subscriptionName;
if ($subscriptionMailingAddress != '') {
$subscriptionContactSignature .= "\n" . $subscriptionMailingAddress;
}
if ($subscriptionPhone != '') {
$subscriptionContactSignature .= "\n" . __('user.phone') . ': ' . $subscriptionPhone;
}
return $subscriptionContactSignature . "\n" . __('user.email') . ': ' . $subscriptionEmail;
}
}
@@ -0,0 +1,64 @@
<?php
/**
* @file classes/migration/install/JournalsMigration.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class JournalsMigration
*
* @brief Describe database table structures.
*/
namespace APP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class JournalsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Journals and basic journal settings.
Schema::create('journals', function (Blueprint $table) {
$table->comment('A list of all journals in the installation of OJS.');
$table->bigInteger('journal_id')->autoIncrement();
$table->string('path', 32);
$table->float('seq', 8, 2)->default(0)->comment('Used to order lists of journals');
$table->string('primary_locale', 14);
$table->smallInteger('enabled')->default(1)->comment('Controls whether or not the journal is considered "live" and will appear on the website. (Note that disabled journals may still be accessible, but only if the user knows the URL.)');
$table->unique(['path'], 'journals_path');
$table->bigInteger('current_issue_id')->nullable()->default(null);
});
// Journal settings.
Schema::create('journal_settings', function (Blueprint $table) {
$table->comment('More data about journals, including localized properties like policies.');
$table->bigIncrements('journal_setting_id');
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'journal_settings_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'journal_settings_journal_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['journal_id', 'locale', 'setting_name'], 'journal_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('journal_settings');
Schema::drop('journals');
}
}
@@ -0,0 +1,422 @@
<?php
/**
* @file classes/migration/install/MetricsMigration.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MetricsMigration
*
* @brief Describe database table structures.
*/
namespace APP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema as Schema;
class MetricsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('metrics_context', function (Blueprint $table) {
$table->comment('Daily statistics for views of the homepage.');
$table->bigIncrements('metrics_context_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'metrics_context_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_context_context_id');
$table->date('date');
$table->integer('metric');
});
Schema::create('metrics_submission', function (Blueprint $table) {
$table->comment('Daily statistics for views and downloads of published submissions and galleys.');
$table->bigIncrements('metrics_submission_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'ms_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'metrics_submission_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'metrics_submission_submission_file_id');
$table->bigInteger('file_type')->nullable();
$table->bigInteger('assoc_type');
$table->date('date');
$table->integer('metric');
$table->index(['context_id', 'submission_id', 'assoc_type', 'file_type'], 'ms_context_id_submission_id_assoc_type_file_type');
});
Schema::create('metrics_issue', function (Blueprint $table) {
$table->comment('Daily statistics for views and downloads of published issues.');
$table->bigIncrements('metrics_issue_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'metrics_issue_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_issue_context_id');
$table->bigInteger('issue_id');
$table->foreign('issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'metrics_issue_issue_id');
$table->bigInteger('issue_galley_id')->nullable();
$table->foreign('issue_galley_id')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
$table->index(['issue_galley_id'], 'metrics_issue_issue_galley_id');
$table->date('date');
$table->integer('metric');
$table->index(['context_id', 'issue_id'], 'metrics_issue_context_id_issue_id');
});
Schema::create('metrics_counter_submission_daily', function (Blueprint $table) {
$table->comment('Daily statistics matching the COUNTER R5 protocol for views and downloads of published submissions and galleys.');
$table->bigIncrements('metrics_counter_submission_daily_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'msd_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msd_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msd_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_daily_submission_id');
$table->date('date');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msd_context_id_submission_id');
$table->unique(['load_id', 'context_id', 'submission_id', 'date'], 'msd_uc_load_id_context_id_submission_id_date');
});
Schema::create('metrics_counter_submission_monthly', function (Blueprint $table) {
$table->comment('Monthly statistics matching the COUNTER R5 protocol for views and downloads of published submissions and galleys.');
$table->bigIncrements('metrics_counter_submission_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msm_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msm_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_monthly_submission_id');
$table->integer('month');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msm_context_id_submission_id');
$table->unique(['context_id', 'submission_id', 'month'], 'msm_uc_context_id_submission_id_month');
});
Schema::create('metrics_counter_submission_institution_daily', function (Blueprint $table) {
$table->comment('Daily statistics matching the COUNTER R5 protocol for views and downloads from institutions.');
$table->bigIncrements('metrics_counter_submission_institution_daily_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'msid_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msid_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_institution_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msid_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_institution_daily_submission_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'msid_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'metrics_counter_submission_institution_daily_institution_id');
$table->date('date');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msid_context_id_submission_id');
$table->unique(['load_id', 'context_id', 'submission_id', 'institution_id', 'date'], 'msid_uc_load_id_context_id_submission_id_institution_id_date');
});
Schema::create('metrics_counter_submission_institution_monthly', function (Blueprint $table) {
$table->comment('Monthly statistics matching the COUNTER R5 protocol for views and downloads from institutions.');
$table->bigIncrements('metrics_counter_submission_institution_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msim_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_institution_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msim_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_institution_monthly_submission_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'msim_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'metrics_counter_submission_institution_monthly_institution_id');
$table->integer('month');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msim_context_id_submission_id');
$table->unique(['context_id', 'submission_id', 'institution_id', 'month'], 'msim_uc_context_id_submission_id_institution_id_month');
});
Schema::create('metrics_submission_geo_daily', function (Blueprint $table) {
$table->comment('Daily statistics by country, region and city for views and downloads of published submissions and galleys.');
$table->bigIncrements('metrics_submission_geo_daily_id');
$table->string('load_id', 50);
$table->index(['load_id'], 'msgd_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msgd_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_geo_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msgd_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_geo_daily_submission_id');
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->date('date');
$table->integer('metric');
$table->integer('metric_unique');
$table->index(['context_id', 'submission_id'], 'msgd_context_id_submission_id');
switch (DB::getDriverName()) {
case 'mysql':
// See "Create a database table" here: https://db-ip.com/db/format/ip-to-city-lite/csv.html
// where city is defined as varchar(80)
$table->unique([DB::raw('load_id, context_id, submission_id, country, region, city(80), date')], 'msgd_uc_load_context_submission_c_r_c_date');
break;
case 'pgsql':
$table->unique(['load_id', 'context_id', 'submission_id', 'country', 'region', 'city', 'date'], 'msgd_uc_load_context_submission_c_r_c_date');
break;
}
});
Schema::create('metrics_submission_geo_monthly', function (Blueprint $table) {
$table->comment('Monthly statistics by country, region and city for views and downloads of published submissions and galleys.');
$table->bigIncrements('metrics_submission_geo_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msgm_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_geo_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msgm_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_geo_monthly_submission_id');
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->integer('month');
$table->integer('metric');
$table->integer('metric_unique');
$table->index(['context_id', 'submission_id'], 'msgm_context_id_submission_id');
switch (DB::getDriverName()) {
case 'mysql':
// See "Create a database table" here: https://db-ip.com/db/format/ip-to-city-lite/csv.html
// where city is defined as varchar(80)
$table->unique([DB::raw('context_id, submission_id, country, region, city(80), month')], 'msgm_uc_context_submission_c_r_c_month');
break;
case 'pgsql':
$table->unique(['context_id', 'submission_id', 'country', 'region', 'city', 'month'], 'msgm_uc_context_submission_c_r_c_month');
break;
}
});
// Usage stats total item temporary records
Schema::create('usage_stats_total_temporary_records', function (Blueprint $table) {
$table->comment('Temporary stats totals based on visitor log records. Data in this table is provisional. See the metrics_* tables for compiled stats.');
$table->bigIncrements('usage_stats_temp_total_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 64);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->string('canonical_url', 255);
$table->bigInteger('issue_id')->nullable();
$table->foreign('issue_id', 'ust_issue_id_foreign')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'usage_stats_total_temporary_records_issue_id');
$table->bigInteger('issue_galley_id')->nullable();
$table->foreign('issue_galley_id', 'ust_issue_galley_id_foreign')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
$table->index(['issue_galley_id'], 'usage_stats_total_temporary_records_issue_galley_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'ust_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usage_stats_total_temporary_records_context_id');
$table->bigInteger('submission_id')->nullable();
$table->foreign('submission_id', 'ust_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usage_stats_total_temporary_records_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'ust_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usage_stats_total_temporary_records_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'ust_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usage_stats_total_temporary_records_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 50);
$table->index(['load_id', 'context_id', 'ip'], 'ust_load_id_context_id_ip');
});
// Usage stats unique item investigations temporary records
// No need to consider issue_id and issue_galley_id here because
// investigations are only relevant/calculated on submission level.
Schema::create('usage_stats_unique_item_investigations_temporary_records', function (Blueprint $table) {
$table->comment('Temporary stats on unique downloads based on visitor log records. Data in this table is provisional. See the metrics_* tables for compiled stats.');
$table->bigIncrements('usage_stats_temp_unique_item_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 64);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->bigInteger('context_id');
$table->foreign('context_id', 'usii_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usii_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'usii_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usii_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'usii_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usii_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'usii_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usii_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 50);
$table->index(['load_id', 'context_id', 'ip'], 'usii_load_id_context_id_ip');
});
// Usage stats unique item requests temporary records
// No need to consider issue_id and issue_galley_id here because
// requests are only relevant/calculated on submission level.
Schema::create('usage_stats_unique_item_requests_temporary_records', function (Blueprint $table) {
$table->comment('Temporary stats on unique views based on visitor log records. Data in this table is provisional. See the metrics_* tables for compiled stats.');
$table->bigIncrements('usage_stats_temp_item_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 64);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->bigInteger('context_id');
$table->foreign('context_id', 'usir_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usir_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'usir_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usir_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'usir_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usir_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'usir_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usir_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 50);
$table->index(['load_id', 'context_id', 'ip'], 'usir_load_id_context_id_ip');
});
// Usage stats institution temporary records
// This table is needed because of data normalization
Schema::create('usage_stats_institution_temporary_records', function (Blueprint $table) {
$table->comment('Temporary stats for views and downloads from institutions based on visitor log records. Data in this table is provisional. See the metrics_* tables for compiled stats.');
$table->bigIncrements('usage_stats_temp_institution_id');
$table->string('load_id', 50);
$table->bigInteger('line_number');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'usi_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'usi_institution_id');
$table->unique(['load_id', 'line_number', 'institution_id'], 'usitr_load_id_line_number_institution_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('metrics_context');
Schema::drop('metrics_submission');
Schema::drop('metrics_issue');
Schema::drop('metrics_counter_submission_daily');
Schema::drop('metrics_counter_submission_monthly');
Schema::drop('metrics_counter_submission_institution_daily');
Schema::drop('metrics_counter_submission_institution_monthly');
Schema::drop('metrics_submission_geo_daily');
Schema::drop('metrics_submission_geo_monthly');
Schema::drop('usage_stats_total_temporary_records');
Schema::drop('usage_stats_unique_item_investigations_temporary_records');
Schema::drop('usage_stats_unique_item_requests_temporary_records');
Schema::drop('usage_stats_institution_temporary_records');
}
}
+455
View File
@@ -0,0 +1,455 @@
<?php
/**
* @file classes/migration/install/OJSMigration.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class OJSMigration
*
* @brief Describe database table structures.
*/
namespace APP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class OJSMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Journal sections.
Schema::create('sections', function (Blueprint $table) {
$table->comment('A list of all sections into which submissions can be organized, forming the table of contents.');
$table->bigInteger('section_id')->autoIncrement();
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'sections_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'sections_journal_id');
$table->bigInteger('review_form_id')->nullable();
$table->foreign('review_form_id', 'sections_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('set null');
$table->index(['review_form_id'], 'sections_review_form_id');
$table->float('seq', 8, 2)->default(0);
$table->smallInteger('editor_restricted')->default(0);
$table->smallInteger('meta_indexed')->default(0);
$table->smallInteger('meta_reviewed')->default(1);
$table->smallInteger('abstracts_not_required')->default(0);
$table->smallInteger('hide_title')->default(0);
$table->smallInteger('hide_author')->default(0);
$table->smallInteger('is_inactive')->default(0);
$table->bigInteger('abstract_word_count')->nullable();
});
// Section-specific settings
Schema::create('section_settings', function (Blueprint $table) {
$table->comment('More data about sections, including localized properties like section titles.');
$table->bigIncrements('section_setting_id');
$table->bigInteger('section_id');
$table->foreign('section_id', 'section_settings_section_id')->references('section_id')->on('sections')->onDelete('cascade');
$table->index(['section_id'], 'section_settings_section_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['section_id', 'locale', 'setting_name'], 'section_settings_unique');
});
// Journal issues.
Schema::create('issues', function (Blueprint $table) {
$table->comment('A list of all journal issues, with identifying information like year, number, volume, etc.');
$table->bigInteger('issue_id')->autoIncrement();
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'issues_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'issues_journal_id');
$table->smallInteger('volume')->nullable();
$table->string('number', 40)->nullable();
$table->smallInteger('year')->nullable();
$table->smallInteger('published')->default(0);
$table->datetime('date_published')->nullable();
$table->datetime('date_notified')->nullable();
$table->datetime('last_modified')->nullable();
$table->smallInteger('access_status')->default(1);
$table->datetime('open_access_date')->nullable();
$table->smallInteger('show_volume')->default(0);
$table->smallInteger('show_number')->default(0);
$table->smallInteger('show_year')->default(0);
$table->smallInteger('show_title')->default(0);
$table->string('style_file_name', 90)->nullable();
$table->string('original_style_file_name', 255)->nullable();
$table->string('url_path', 64)->nullable();
$table->bigInteger('doi_id')->nullable();
$table->foreign('doi_id')->references('doi_id')->on('dois')->nullOnDelete();
$table->index(['doi_id'], 'issues_doi_id');
$table->index(['url_path'], 'issues_url_path');
});
// Locale-specific issue data
Schema::create('issue_settings', function (Blueprint $table) {
$table->comment('More data about issues, including localized properties such as issue titles.');
$table->bigIncrements('issue_setting_id');
$table->bigInteger('issue_id');
$table->foreign('issue_id', 'issue_settings_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'issue_settings_issue_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['issue_id', 'locale', 'setting_name'], 'issue_settings_unique');
});
// Add partial index (DBMS-specific)
switch (DB::getDriverName()) {
case 'mysql': DB::unprepared('CREATE INDEX issue_settings_name_value ON issue_settings (setting_name(50), setting_value(150))');
break;
case 'pgsql': DB::unprepared("CREATE INDEX issue_settings_name_value ON issue_settings (setting_name, setting_value) WHERE setting_name IN ('medra::registeredDoi', 'datacite::registeredDoi')");
break;
}
Schema::create('issue_files', function (Blueprint $table) {
$table->comment('Relationships between issues and issue files, such as cover images.');
$table->bigInteger('file_id')->autoIncrement();
$table->bigInteger('issue_id');
$table->foreign('issue_id', 'issue_files_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'issue_files_issue_id');
$table->string('file_name', 90);
$table->string('file_type', 255);
$table->bigInteger('file_size');
$table->bigInteger('content_type');
$table->string('original_file_name', 127)->nullable();
$table->datetime('date_uploaded');
$table->datetime('date_modified');
});
// Issue galleys.
Schema::create('issue_galleys', function (Blueprint $table) {
$table->comment('Issue galleys are representations of the entire issue in a single file, such as a complete issue PDF.');
$table->bigInteger('galley_id')->autoIncrement();
$table->string('locale', 14)->nullable();
$table->bigInteger('issue_id');
$table->foreign('issue_id', 'issue_galleys_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'issue_galleys_issue_id');
$table->bigInteger('file_id');
$table->foreign('file_id', 'issue_galleys_file_id')->references('file_id')->on('issue_files')->onDelete('cascade');
$table->index(['file_id'], 'issue_galleys_file_id');
$table->string('label', 255)->nullable();
$table->float('seq', 8, 2)->default(0);
$table->string('url_path', 64)->nullable();
$table->index(['url_path'], 'issue_galleys_url_path');
});
// Issue galley metadata.
Schema::create('issue_galley_settings', function (Blueprint $table) {
$table->comment('More data about issue galleys, including localized content such as labels.');
$table->bigIncrements('issue_galley_setting_id');
$table->bigInteger('galley_id');
$table->foreign('galley_id', 'issue_galleys_settings_galley_id')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
$table->index(['galley_id'], 'issue_galley_settings_galley_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6)->comment('(bool|int|float|string|object)');
$table->unique(['galley_id', 'locale', 'setting_name'], 'issue_galley_settings_unique');
});
Schema::create('custom_issue_orders', function (Blueprint $table) {
$table->comment('Ordering information for the issue list, when custom issue ordering is specified.');
$table->bigIncrements('custom_issue_order_id');
$table->bigInteger('issue_id');
$table->foreign('issue_id', 'custom_issue_orders_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'custom_issue_orders_issue_id');
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'custom_issue_orders_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'custom_issue_orders_journal_id');
$table->float('seq', 8, 2)->default(0);
$table->unique(['issue_id'], 'custom_issue_orders_unique');
});
Schema::create('custom_section_orders', function (Blueprint $table) {
$table->comment('Ordering information for sections within issues, when issue-specific section ordering is specified.');
$table->bigIncrements('custom_section_order_id');
$table->bigInteger('issue_id');
$table->foreign('issue_id', 'custom_section_orders_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'custom_section_orders_issue_id');
$table->bigInteger('section_id');
$table->foreign('section_id', 'custom_section_orders_section_id')->references('section_id')->on('sections')->onDelete('cascade');
$table->index(['section_id'], 'custom_section_orders_section_id');
$table->float('seq', 8, 2)->default(0);
$table->unique(['issue_id', 'section_id'], 'custom_section_orders_unique');
});
// Publications
Schema::create('publications', function (Blueprint $table) {
$table->comment('Each publication is one version of a submission.');
$table->bigInteger('publication_id')->autoIncrement();
$table->bigInteger('access_status')->default(0)->nullable();
$table->date('date_published')->nullable();
$table->datetime('last_modified')->nullable();
$table->bigInteger('primary_contact_id')->nullable();
$table->foreign('primary_contact_id', 'publications_primary_contact_id')->references('author_id')->on('authors')->onDelete('set null');
$table->index(['primary_contact_id'], 'publications_primary_contact_id');
$table->bigInteger('section_id')->nullable();
$table->foreign('section_id', 'publications_section_id')->references('section_id')->on('sections')->onDelete('set null');
$table->index(['section_id'], 'publications_section_id');
$table->float('seq', 8, 2)->default(0);
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'publications_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'publications_submission_id');
$table->smallInteger('status')->default(1); // PKPSubmission::STATUS_QUEUED
$table->string('url_path', 64)->nullable();
$table->bigInteger('version')->nullable();
$table->bigInteger('doi_id')->nullable();
$table->foreign('doi_id')->references('doi_id')->on('dois')->nullOnDelete();
$table->index(['doi_id'], 'publications_doi_id');
$table->index(['url_path'], 'publications_url_path');
});
// The following foreign key relationships are for tables defined in SubmissionsMigration
// but they depend on publications to exist so are created here.
Schema::table('submissions', function (Blueprint $table) {
$table->foreign('current_publication_id', 'submissions_publication_id')->references('publication_id')->on('publications')->onDelete('set null');
$table->index(['current_publication_id'], 'submissions_publication_id');
});
Schema::table('publication_settings', function (Blueprint $table) {
$table->foreign('publication_id', 'publication_settings_publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'publication_settings_publication_id');
});
Schema::table('authors', function (Blueprint $table) {
$table->foreign('publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'authors_publication_id');
});
// Publication galleys
Schema::create('publication_galleys', function (Blueprint $table) {
$table->comment('Publication galleys are representations of a publication in a specific format, e.g. a PDF.');
$table->bigInteger('galley_id')->autoIncrement();
$table->string('locale', 14)->nullable();
$table->bigInteger('publication_id');
$table->foreign('publication_id', 'publication_galleys_publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'publication_galleys_publication_id');
$table->string('label', 255)->nullable();
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
$table->index(['submission_file_id'], 'publication_galleys_submission_file_id');
$table->float('seq', 8, 2)->default(0);
$table->string('remote_url', 2047)->nullable();
$table->smallInteger('is_approved')->default(0);
$table->string('url_path', 64)->nullable();
$table->bigInteger('doi_id')->nullable();
$table->foreign('doi_id')->references('doi_id')->on('dois')->nullOnDelete();
$table->index(['doi_id'], 'publication_galleys_doi_id');
$table->index(['url_path'], 'publication_galleys_url_path');
});
// Galley metadata.
Schema::create('publication_galley_settings', function (Blueprint $table) {
$table->comment('More data about publication galleys, including localized content such as labels.');
$table->bigIncrements('publication_galley_setting_id');
$table->bigInteger('galley_id');
$table->foreign('galley_id', 'publication_galley_settings_galley_id')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['galley_id'], 'publication_galley_settings_galley_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['galley_id', 'locale', 'setting_name'], 'publication_galley_settings_unique');
});
// Add partial index (DBMS-specific)
switch (DB::getDriverName()) {
case 'mysql': DB::unprepared('CREATE INDEX publication_galley_settings_name_value ON publication_galley_settings (setting_name(50), setting_value(150))');
break;
case 'pgsql': DB::unprepared('CREATE INDEX publication_galley_settings_name_value ON publication_galley_settings (setting_name, setting_value)');
break;
}
// Subscription types.
Schema::create('subscription_types', function (Blueprint $table) {
$table->comment('Subscription types represent the kinds of subscriptions that a user or institution may have, such as an annual subscription or a discounted subscription.');
$table->bigInteger('type_id')->autoIncrement();
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'subscription_types_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'subscription_types_journal_id');
$table->float('cost', 8, 2);
$table->string('currency_code_alpha', 3);
$table->smallInteger('duration')->nullable();
$table->smallInteger('format');
$table->smallInteger('institutional')->default(0);
$table->smallInteger('membership')->default(0);
$table->smallInteger('disable_public_display');
$table->float('seq', 8, 2);
});
// Locale-specific subscription type data
Schema::create('subscription_type_settings', function (Blueprint $table) {
$table->comment('More data about subscription types, including localized properties such as names.');
$table->bigIncrements('subscription_type_setting_id');
$table->bigInteger('type_id');
$table->foreign('type_id', 'subscription_type_settings_type_id')->references('type_id')->on('subscription_types')->onDelete('cascade');
$table->index(['type_id'], 'subscription_type_settings_type_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['type_id', 'locale', 'setting_name'], 'subscription_type_settings_unique');
});
// Journal subscriptions.
Schema::create('subscriptions', function (Blueprint $table) {
$table->comment('A list of subscriptions, both institutional and individual, for journals that use subscription-based publishing.');
$table->bigInteger('subscription_id')->autoIncrement();
$table->bigInteger('journal_id');
$table->foreign('journal_id', 'subscriptions_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'subscriptions_journal_id');
$table->bigInteger('user_id');
$table->foreign('user_id', 'subscriptions_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'subscriptions_user_id');
$table->bigInteger('type_id');
$table->foreign('type_id', 'subscriptions_type_id')->references('type_id')->on('subscription_types')->onDelete('cascade');
$table->index(['type_id'], 'subscriptions_type_id');
$table->date('date_start')->nullable();
$table->datetime('date_end')->nullable();
$table->smallInteger('status')->default(1);
$table->string('membership', 40)->nullable();
$table->string('reference_number', 40)->nullable();
$table->text('notes')->nullable();
});
// Journal institutional subscriptions.
Schema::create('institutional_subscriptions', function (Blueprint $table) {
$table->comment('A list of institutional subscriptions, linking a subscription with an institution.');
$table->bigInteger('institutional_subscription_id')->autoIncrement();
$table->bigInteger('subscription_id');
$table->foreign('subscription_id', 'institutional_subscriptions_subscription_id')->references('subscription_id')->on('subscriptions')->onDelete('cascade');
$table->index(['subscription_id'], 'institutional_subscriptions_subscription_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'institutional_subscriptions_institution_id');
$table->string('mailing_address', 255)->nullable();
$table->string('domain', 255)->nullable();
$table->index(['domain'], 'institutional_subscriptions_domain');
});
// Logs queued (unfulfilled) payments.
Schema::create('queued_payments', function (Blueprint $table) {
$table->comment('Unfulfilled (queued) payments, i.e. payments that have not yet been completed via an online payment system.');
$table->bigInteger('queued_payment_id')->autoIncrement();
$table->datetime('date_created');
$table->datetime('date_modified');
$table->date('expiry_date')->nullable();
$table->text('payment_data')->nullable();
});
// Logs completed (fulfilled) payments.
Schema::create('completed_payments', function (Blueprint $table) {
$table->comment('A list of completed (fulfilled) payments relating to a payment type such as a subscription payment.');
$table->bigInteger('completed_payment_id')->autoIncrement();
$table->datetime('timestamp');
$table->bigInteger('payment_type');
$table->bigInteger('context_id');
$table->foreign('context_id', 'completed_payments_context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'completed_payments_context_id');
$table->bigInteger('user_id')->nullable();
$table->foreign('user_id', 'completed_payments_user_id')->references('user_id')->on('users')->onDelete('set null');
$table->index(['user_id'], 'completed_payments_user_id');
$table->bigInteger('assoc_id')->nullable();
$table->float('amount', 8, 2);
$table->string('currency_code_alpha', 3)->nullable();
$table->string('payment_method_plugin_name', 80)->nullable();
});
// Add additional foreign key constraints once all tables have been created
Schema::table('journals', function (Blueprint $table) {
$table->foreign('current_issue_id')->references('issue_id')->on('issues')->onDelete('set null');
$table->index(['current_issue_id'], 'journals_issue_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('completed_payments');
Schema::drop('queued_payments');
Schema::drop('institutional_subscription_ip');
Schema::drop('institutional_subscriptions');
Schema::drop('subscriptions');
Schema::drop('subscription_type_settings');
Schema::drop('subscription_types');
Schema::drop('publication_galley_settings');
Schema::drop('publication_galleys');
Schema::drop('publications');
Schema::drop('custom_section_orders');
Schema::drop('custom_issue_orders');
Schema::drop('issue_files');
Schema::drop('issue_galley_settings');
Schema::drop('issue_galleys');
Schema::drop('issue_settings');
Schema::drop('issues');
Schema::drop('section_settings');
Schema::drop('sections');
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/migration/upgrade/OJSv3_3_0UpgradeMigration.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class OJSv3_3_0UpgradeMigration
*
* @brief Describe database table structures.
*/
namespace APP\migration\upgrade;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class OJSv3_3_0UpgradeMigration extends \PKP\migration\upgrade\PKPv3_3_0UpgradeMigration
{
protected function getSubmissionPath(): string
{
return 'articles';
}
protected function getContextPath(): string
{
return 'journals';
}
protected function getContextTable(): string
{
return 'journals';
}
protected function getContextKeyField(): string
{
return 'journal_id';
}
protected function getContextSettingsTable(): string
{
return 'journal_settings';
}
protected function getSectionTable(): string
{
return 'sections';
}
protected function getSerializedSettings(): array
{
return [
'site_settings' => [
'enableBulkEmails',
'installedLocales',
'pageHeaderTitleImage',
'sidebar',
'styleSheet',
'supportedLocales',
],
'journal_settings' => [
'disableBulkEmailUserGroups',
'favicon',
'homepageImage',
'pageHeaderLogoImage',
'sidebar',
'styleSheet',
'submissionChecklist',
'supportedFormLocales',
'supportedLocales',
'supportedSubmissionLocales',
'enablePublisherId',
'journalThumbnail',
],
'publication_settings' => [
'categoryIds',
'coverImage',
'disciplines',
'keywords',
'languages',
'subjects',
'supportingAgencies',
]
];
}
/**
* Run the migrations.
*/
public function up(): void
{
parent::up();
// pkp/pkp-lib#6807 Make sure all submission/issue last modification dates are set
DB::statement('UPDATE issues SET last_modified = date_published WHERE last_modified IS NULL');
// Delete the old MODS34 filters
DB::statement("DELETE FROM filters WHERE class_name='plugins.metadata.mods34.filter.Mods34SchemaArticleAdapter'");
DB::statement("DELETE FROM filter_groups WHERE symbolic IN ('article=>mods34', 'mods34=>article')");
// Delete mEDRA dependencies
DB::statement("DELETE FROM filters WHERE class_name IN ('plugins.importexport.medra.filter.IssueMedraXmlFilter', 'plugins.importexport.medra.filter.ArticleMedraXmlFilter', 'plugins.importexport.medra.filter.GalleyMedraXmlFilter')");
DB::statement("DELETE FROM filter_groups WHERE symbolic IN ('issue=>medra-xml', 'article=>medra-xml', 'galley=>medra-xml')");
DB::statement("DELETE FROM scheduled_tasks WHERE class_name='plugins.importexport.medra.MedraInfoSender'");
DB::statement("DELETE FROM versions WHERE product_type='plugins.importexport' AND product='medra'");
}
/**
* Complete specific submission file migrations
*
* The main submission file migration is done in
* PKPv3_3_0UpgradeMigration and that migration must
* be run before this one.
*/
protected function migrateSubmissionFiles()
{
parent::migrateSubmissionFiles();
Schema::table('publication_galleys', function (Blueprint $table) {
$table->renameColumn('file_id', 'submission_file_id');
});
DB::statement('UPDATE publication_galleys SET submission_file_id = NULL WHERE submission_file_id = 0');
// pkp/pkp-lib#6616 Delete publication_galleys entries that correspond to nonexistent submission_files
$orphanedIds = DB::table('publication_galleys AS pg')
->leftJoin('submission_files AS sf', 'pg.submission_file_id', '=', 'sf.submission_file_id')
->whereNull('sf.submission_file_id')
->whereNotNull('pg.submission_file_id')
->pluck('pg.submission_file_id', 'pg.galley_id');
foreach ($orphanedIds as $galleyId => $submissionFileId) {
error_log("Removing orphaned publication_galleys entry ID {$galleyId} with submission_file_id {$submissionFileId}");
DB::table('publication_galleys')->where('galley_id', '=', $galleyId)->delete();
}
Schema::table('publication_galleys', function (Blueprint $table) {
$table->bigInteger('submission_file_id')->nullable()->unsigned()->change();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
});
}
}
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I3573_AddPrimaryKeys.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I3573_AddPrimaryKeys.php
*
* @brief Add primary keys to tables that do not have them, to better support database replication.
*
*/
namespace APP\migration\upgrade\v3_4_0;
class I3573_AddPrimaryKeys extends \PKP\migration\upgrade\v3_4_0\I3573_AddPrimaryKeys
{
public static function getKeyNames(): array
{
return array_merge(parent::getKeyNames(), [
'issue_galley_settings' => 'issue_galley_setting_id',
'issue_settings' => 'issue_setting_id',
'journal_settings' => 'journal_setting_id',
'publication_galley_settings' => 'publication_galley_setting_id',
'section_settings' => 'section_setting_id',
'subscription_type_settings' => 'subscription_type_setting_id',
'usage_stats_unique_item_requests_temporary_records' => 'usage_stats_temp_item_id',
'metrics_context' => 'metrics_context_id',
'metrics_counter_submission_institution_daily' => 'metrics_counter_submission_institution_daily_id',
'metrics_counter_submission_daily' => 'metrics_counter_submission_daily_id',
'metrics_submission' => 'metrics_submission_id',
'usage_stats_unique_item_investigations_temporary_records' => 'usage_stats_temp_unique_item_id',
'metrics_counter_submission_monthly' => 'metrics_counter_submission_monthly_id',
'usage_stats_total_temporary_records' => 'usage_stats_temp_total_id',
'usage_stats_institution_temporary_records' => 'usage_stats_temp_institution_id',
'metrics_submission_geo_daily' => 'metrics_submission_geo_daily_id',
'metrics_counter_submission_institution_monthly' => 'metrics_counter_submission_institution_monthly_id',
'metrics_issue' => 'metrics_issue_id',
'metrics_submission_geo_monthly' => 'metrics_submission_geo_monthly_id',
'custom_section_orders' => 'custom_section_order_id',
'custom_issue_orders' => 'custom_issue_order_id',
'funder_settings' => 'funder_setting_id', // PLUGIN
'funder_award_settings' => 'funder_award_setting_id', // PLUGIN
]);
}
public static function getIndexData(): array
{
return array_merge(parent::getIndexData(), [
'journal_settings' => ['journal_settings_pkey', ['journal_id', 'locale', 'setting_name'], 'journal_settings_unique', true],
'section_settings' => ['section_settings_pkey', ['section_id', 'locale', 'setting_name'], 'section_settings_unique', true],
'issue_settings' => ['issue_settings_pkey', ['issue_id', 'locale', 'setting_name'], 'issue_settings_unique', true],
'issue_galley_settings' => ['issue_galley_settings_pkey', ['galley_id', 'locale', 'setting_name'], 'issue_galley_settings_unique', true],
'custom_issue_orders' => ['custom_issue_orders_pkey', ['issue_id'], 'custom_issue_orders_unique', true],
'custom_section_orders' => ['custom_section_orders_pkey', ['issue_id', 'section_id'], 'custom_section_orders_unique', true],
'publication_galley_settings' => ['publication_galley_settings_pkey', ['galley_id', 'locale', 'setting_name'], 'publication_galley_settings_unique', true],
'subscription_type_settings' => ['subscription_type_settings_pkey', ['type_id', 'locale', 'setting_name'], 'subscription_type_settings_unique', true],
]);
}
}
@@ -0,0 +1,45 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I4235_OAISetSpec.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I4235_OAISetSpec
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\oai\OAIUtils;
class I4235_OAISetSpec extends \PKP\migration\Migration
{
/**
* Run the migration.
*/
public function up(): void
{
// pkp/pkp-lib/issues/4235 Improve OAI-PMH set spec compliance
// Convert stored setSpec strings to valid format
$setSpecs = DB::table('data_object_tombstones')->select('set_spec')->distinct()->get()->toArray();
foreach ($setSpecs as $row) {
$a = preg_split('/:/', $row->set_spec);
if (count($a) == 2) {
[$journalSpec, $sectionSpec] = $a;
$new = OAIUtils::toValidSetSpec(urldecode($journalSpec)) . ':' . OAIUtils::toValidSetSpec(urldecode($sectionSpec));
DB::table('data_object_tombstones')->where('set_spec', $row->set_spec)->update(['set_spec' => $new]);
}
}
}
/**
* Reverse the downgrades
*/
public function down(): void
{
// The old format is not recoverable since some characters might have been stripped
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I5716_EmailTemplateAssignments.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I5716_EmailTemplateAssignments
*
* @brief Refactors relationship between Mailables and Email Templates
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Collection;
use PKP\mail\mailables\DiscussionCopyediting;
use PKP\mail\mailables\DiscussionProduction;
use PKP\mail\mailables\DiscussionReview;
use PKP\mail\mailables\DiscussionSubmission;
class I5716_EmailTemplateAssignments extends \PKP\migration\upgrade\v3_4_0\I5716_EmailTemplateAssignments
{
protected function getContextTable(): string
{
return 'journals';
}
protected function getContextSettingsTable(): string
{
return 'journal_settings';
}
protected function getContextIdColumn(): string
{
return 'journal_id';
}
protected function getDiscussionTemplates(): Collection
{
return collect([
DiscussionSubmission::getEmailTemplateKey(),
DiscussionReview::getEmailTemplateKey(),
DiscussionCopyediting::getEmailTemplateKey(),
DiscussionProduction::getEmailTemplateKey(),
]);
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6091_AddFilterNamespaces.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6091_AddFilterNamespaces
*
* @brief Describe upgrade/downgrade operations for introducing namespaces to the built-in set of filters.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
class I6091_AddFilterNamespaces extends \PKP\migration\Migration
{
public const FILTER_RENAME_MAP = [
// Application filters
'plugins.importexport.doaj.filter.DOAJXmlFilter' => 'APP\plugins\importexport\doaj\filter\DOAJXmlFilter',
'plugins.generic.datacite.filter.DataciteXmlFilter' => 'APP\plugins\generic\datacite\filter\DataciteXmlFilter',
'plugins.importexport.native.filter.ArticleNativeXmlFilter' => 'APP\plugins\importexport\native\filter\ArticleNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlArticleFilter' => 'APP\plugins\importexport\native\filter\NativeXmlArticleFilter',
'plugins.importexport.native.filter.IssueNativeXmlFilter' => 'APP\plugins\importexport\native\filter\IssueNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlIssueFilter' => 'APP\plugins\importexport\native\filter\NativeXmlIssueFilter',
'plugins.importexport.native.filter.IssueGalleyNativeXmlFilter' => 'APP\plugins\importexport\native\filter\IssueGalleyNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlIssueGalleyFilter' => 'APP\plugins\importexport\native\filter\NativeXmlIssueGalleyFilter',
'plugins.importexport.native.filter.AuthorNativeXmlFilter' => 'APP\plugins\importexport\native\filter\AuthorNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlAuthorFilter' => 'APP\plugins\importexport\native\filter\NativeXmlAuthorFilter',
'plugins.importexport.native.filter.NativeXmlArticleFileFilter' => 'APP\plugins\importexport\native\filter\NativeXmlArticleFileFilter',
'plugins.importexport.native.filter.ArticleGalleyNativeXmlFilter' => 'APP\plugins\importexport\native\filter\ArticleGalleyNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlArticleGalleyFilter' => 'APP\plugins\importexport\native\filter\NativeXmlArticleGalleyFilter',
'plugins.importexport.native.filter.PublicationNativeXmlFilter' => 'APP\plugins\importexport\native\filter\PublicationNativeXmlFilter',
'plugins.importexport.native.filter.NativeXmlPublicationFilter' => 'APP\plugins\importexport\native\filter\NativeXmlPublicationFilter',
'plugins.importexport.doaj.filter.DOAJJsonFilter' => 'APP\plugins\importexport\doaj\filter\DOAJJsonFilter',
'plugins.importexport.pubmed.filter.ArticlePubMedXmlFilter' => 'APP\plugins\importexport\pubmed\filter\ArticlePubMedXmlFilter',
'plugins.metadata.dc11.filter.Dc11SchemaArticleAdapter' => 'APP\plugins\metadata\dc11\filter\Dc11SchemaArticleAdapter',
'plugins.generic.crossref.filter.IssueCrossrefXmlFilter' => 'APP\plugins\generic\crossref\filter\IssueCrossrefXmlFilter',
'plugins.generic.crossref.filter.ArticleCrossrefXmlFilter' => 'APP\plugins\generic\crossref\filter\ArticleCrossrefXmlFilter',
// pkp-lib filters
'lib.pkp.plugins.importexport.users.filter.PKPUserUserXmlFilter' => 'PKP\plugins\importexport\users\filter\PKPUserUserXmlFilter',
'lib.pkp.plugins.importexport.users.filter.UserXmlPKPUserFilter' => 'PKP\plugins\importexport\users\filter\UserXmlPKPUserFilter',
'lib.pkp.plugins.importexport.users.filter.UserGroupNativeXmlFilter' => 'PKP\plugins\importexport\users\filter\UserGroupNativeXmlFilter',
'lib.pkp.plugins.importexport.users.filter.NativeXmlUserGroupFilter' => 'PKP\plugins\importexport\users\filter\NativeXmlUserGroupFilter',
'lib.pkp.plugins.importexport.native.filter.SubmissionFileNativeXmlFilter' => 'PKP\plugins\importexport\native\filter\SubmissionFileNativeXmlFilter',
];
public const TASK_RENAME_MAP = [
'lib.pkp.classes.task.ReviewReminder' => 'PKP\task\ReviewReminder',
'lib.pkp.classes.task.StatisticsReport' => 'PKP\task\StatisticsReport',
'classes.tasks.SubscriptionExpiryReminder' => 'APP\tasks\SubscriptionExpiryReminder',
'lib.pkp.classes.task.DepositDois' => 'PKP\task\DepositDois',
'lib.pkp.classes.task.RemoveUnvalidatedExpiredUsers' => 'PKP\task\RemoveUnvalidatedExpiredUsers',
'lib.pkp.classes.task.EditorialReminders' => 'PKP\task\EditorialReminders',
'lib.pkp.classes.task.UpdateIPGeoDB' => 'PKP\task\UpdateIPGeoDB',
'classes.tasks.UsageStatsLoader' => 'APP\tasks\UsageStatsLoader',
'plugins.importexport.doaj.DOAJInfoSender' => 'APP\plugins\importexport\doaj\DOAJInfoSender',
];
/**
* Run the migration.
*/
public function up(): void
{
foreach (self::FILTER_RENAME_MAP as $oldName => $newName) {
DB::statement('UPDATE filters SET class_name = ? WHERE class_name = ?', [$newName, $oldName]);
}
foreach (self::TASK_RENAME_MAP as $oldName => $newName) {
DB::statement('UPDATE scheduled_tasks SET class_name = ? WHERE class_name = ?', [$newName, $oldName]);
}
DB::statement('UPDATE filter_groups SET output_type=? WHERE output_type = ?', ['metadata::APP\plugins\metadata\dc11\schema\Dc11Schema(ARTICLE)', 'metadata::plugins.metadata.dc11.schema.Dc11Schema(ARTICLE)']);
}
/**
* Reverse the downgrades
*/
public function down(): void
{
foreach (self::FILTER_RENAME_MAP as $oldName => $newName) {
DB::statement('UPDATE filters SET class_name = ? WHERE class_name = ?', [$oldName, $newName]);
}
foreach (self::TASK_RENAME_MAP as $oldName => $newName) {
DB::statement('UPDATE scheduled_tasks SET class_name = ? WHERE class_name = ?', [$oldName, $newName]);
}
DB::statement('UPDATE filter_groups SET output_type=? WHERE output_type = ?', ['metadata::plugins.metadata.dc11.schema.Dc11Schema(ARTICLE)', 'metadata::APP\plugins\metadata\dc11\schema\Dc11Schema(ARTICLE)']);
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6093_AddForeignKeys.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6093_AddForeignKeys
*
* @brief Describe upgrade/downgrade operations for introducing foreign key definitions to existing database relationships.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class I6093_AddForeignKeys extends \PKP\migration\upgrade\v3_4_0\I6093_AddForeignKeys
{
protected function getContextTable(): string
{
return 'journals';
}
protected function getContextKeyField(): string
{
return 'journal_id';
}
protected function getContextSettingsTable(): string
{
return 'journal_settings';
}
public function up(): void
{
parent::up();
Schema::table('sections', function (Blueprint $table) {
$table->foreign('review_form_id', 'sections_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('set null');
$table->index(['review_form_id'], 'sections_review_form_id');
$table->foreign('journal_id', 'sections_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
});
Schema::table('section_settings', function (Blueprint $table) {
$table->foreign('section_id', 'section_settings_section_id')->references('section_id')->on('sections')->onDelete('cascade');
});
Schema::table('issues', function (Blueprint $table) {
$table->foreign('journal_id', 'issues_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
});
Schema::table('issue_settings', function (Blueprint $table) {
$table->foreign('issue_id', 'issue_settings_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
});
Schema::table('issue_files', function (Blueprint $table) {
$table->foreign('issue_id', 'issue_files_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
});
Schema::table('issue_galleys', function (Blueprint $table) {
$table->foreign('issue_id', 'issue_galleys_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->foreign('file_id', 'issue_galleys_file_id')->references('file_id')->on('issue_files')->onDelete('cascade');
$table->index(['file_id'], 'issue_galleys_file_id');
});
Schema::table('issue_galley_settings', function (Blueprint $table) {
$table->foreign('galley_id', 'issue_galleys_settings_galley_id')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
});
Schema::table('custom_issue_orders', function (Blueprint $table) {
$table->foreign('issue_id', 'custom_issue_orders_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'custom_issue_orders_issue_id');
$table->foreign('journal_id', 'custom_issue_orders_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'custom_issue_orders_journal_id');
});
Schema::table('custom_section_orders', function (Blueprint $table) {
$table->foreign('issue_id', 'custom_section_orders_issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'custom_section_orders_issue_id');
$table->foreign('section_id', 'custom_section_orders_section_id')->references('section_id')->on('sections')->onDelete('cascade');
$table->index(['section_id'], 'custom_section_orders_section_id');
});
Schema::table('publications', function (Blueprint $table) {
$table->foreign('section_id', 'publications_section_id')->references('section_id')->on('sections')->onDelete('set null');
$table->foreign('submission_id', 'publications_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->foreign('primary_contact_id', 'publications_primary_contact_id')->references('author_id')->on('authors')->onDelete('set null');
$table->index(['primary_contact_id'], 'publications_primary_contact_id');
});
// Attempt to drop the previous foreign key, which doesn't have the cascade rule
if (DB::getDoctrineSchemaManager()->introspectTable('publication_galleys')->hasForeignKey('publication_galleys_submission_file_id_foreign')) {
Schema::table('publication_galleys', fn (Blueprint $table) => $table->dropForeign('publication_galleys_submission_file_id_foreign'));
}
Schema::table('publication_galleys', function (Blueprint $table) {
$table->foreign('publication_id', 'publication_galleys_publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['submission_file_id'], 'publication_galleys_submission_file_id');
});
Schema::table('publication_galley_settings', function (Blueprint $table) {
$table->foreign('galley_id', 'publication_galley_settings_galley_id')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
});
Schema::table('subscription_types', function (Blueprint $table) {
$table->foreign('journal_id', 'subscription_types_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'subscription_types_journal_id');
});
Schema::table('subscription_type_settings', function (Blueprint $table) {
$table->foreign('type_id', 'subscription_type_settings_type_id')->references('type_id')->on('subscription_types')->onDelete('cascade');
});
Schema::table('subscriptions', function (Blueprint $table) {
$table->foreign('journal_id', 'subscriptions_journal_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['journal_id'], 'subscriptions_journal_id');
$table->foreign('user_id', 'subscriptions_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'subscriptions_user_id');
$table->foreign('type_id', 'subscriptions_type_id')->references('type_id')->on('subscription_types')->onDelete('cascade');
$table->index(['type_id'], 'subscriptions_type_id');
});
Schema::table('institutional_subscriptions', function (Blueprint $table) {
$table->foreign('subscription_id', 'institutional_subscriptions_subscription_id')->references('subscription_id')->on('subscriptions')->onDelete('cascade');
});
Schema::table('completed_payments', function (Blueprint $table) {
$table->foreign('context_id', 'completed_payments_context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'completed_payments_context_id');
$table->foreign('user_id', 'completed_payments_user_id')->references('user_id')->on('users')->onDelete('set null');
$table->index(['user_id'], 'completed_payments_user_id');
});
Schema::table('journals', function (Blueprint $table) {
$table->index(['current_issue_id'], 'journals_issue_id');
});
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6241_RequiredGenres.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6241_RequiredGenres
*
* @brief Set a required file genre for this app.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
class I6241_RequiredGenres extends \PKP\migration\upgrade\v3_4_0\I6241_RequiredGenres
{
public function up(): void
{
parent::up();
DB::table('genres')
->where('entry_key', 'SUBMISSION') // "Article Text" from genres.xml
->update(['required' => 1]);
}
}
@@ -0,0 +1,29 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6306_EnableCategories.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6306_EnableCategories
*/
namespace APP\migration\upgrade\v3_4_0;
class I6306_EnableCategories extends \PKP\migration\upgrade\v3_4_0\I6306_EnableCategories
{
protected function getContextTable(): string
{
return 'journals';
}
protected function getContextSettingsTable(): string
{
return 'journal_settings';
}
protected function getContextIdColumn(): string
{
return 'journal_id';
}
}
@@ -0,0 +1,27 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_CleanOldMetrics.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_CleanOldMetrics
*
* @brief Clean the old metrics:
* delete migrated entries with the metric type ojs::counter from the DB table metrics,
* move back the orphaned metrics from the temporary metrics_tmp,
* rename or delete the DB table metrics,
* delete DB table usage_stats_temporary_records.
*/
namespace APP\migration\upgrade\v3_4_0;
class I6782_CleanOldMetrics extends \PKP\migration\upgrade\v3_4_0\I6782_CleanOldMetrics
{
protected function getMetricType(): string
{
return 'ojs::counter';
}
}
@@ -0,0 +1,378 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_CreateNewMetricsTables.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_CreateNewMetricsTables
*
* @brief Describe database table structures.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema as Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I6782_CreateNewMetricsTables extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('metrics_context', function (Blueprint $table) {
$table->bigIncrements('metrics_context_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'metrics_context_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_context_context_id');
$table->date('date');
$table->integer('metric');
});
Schema::create('metrics_submission', function (Blueprint $table) {
$table->bigIncrements('metrics_submission_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'ms_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'metrics_submission_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'metrics_submission_submission_file_id');
$table->bigInteger('file_type')->nullable();
$table->bigInteger('assoc_type');
$table->date('date');
$table->integer('metric');
$table->index(['context_id', 'submission_id', 'assoc_type', 'file_type'], 'ms_context_id_submission_id_assoc_type_file_type');
});
Schema::create('metrics_issue', function (Blueprint $table) {
$table->bigIncrements('metrics_issue_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'metrics_issue_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_issue_context_id');
$table->bigInteger('issue_id');
$table->foreign('issue_id')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'metrics_issue_issue_id');
$table->bigInteger('issue_galley_id')->nullable();
$table->foreign('issue_galley_id')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
$table->index(['issue_galley_id'], 'metrics_issue_issue_galley_id');
$table->date('date');
$table->integer('metric');
$table->index(['context_id', 'issue_id'], 'metrics_issue_context_id_issue_id');
});
Schema::create('metrics_counter_submission_daily', function (Blueprint $table) {
$table->bigIncrements('metrics_counter_submission_daily_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'msd_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msd_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msd_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_daily_submission_id');
$table->date('date');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msd_context_id_submission_id');
$table->unique(['load_id', 'context_id', 'submission_id', 'date'], 'msd_uc_load_id_context_id_submission_id_date');
});
Schema::create('metrics_counter_submission_monthly', function (Blueprint $table) {
$table->bigIncrements('metrics_counter_submission_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msm_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msm_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_monthly_submission_id');
$table->integer('month');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msm_context_id_submission_id');
$table->unique(['context_id', 'submission_id', 'month'], 'msm_uc_context_id_submission_id_month');
});
Schema::create('metrics_counter_submission_institution_daily', function (Blueprint $table) {
$table->bigIncrements('metrics_counter_submission_institution_daily_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'msid_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msid_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_institution_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msid_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_institution_daily_submission_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'msid_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'metrics_counter_submission_institution_daily_institution_id');
$table->date('date');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msid_context_id_submission_id');
$table->unique(['load_id', 'context_id', 'submission_id', 'institution_id', 'date'], 'msid_uc_load_id_context_id_submission_id_institution_id_date');
});
Schema::create('metrics_counter_submission_institution_monthly', function (Blueprint $table) {
$table->bigIncrements('metrics_counter_submission_institution_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msim_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_counter_submission_institution_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msim_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_counter_submission_institution_monthly_submission_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'msim_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'metrics_counter_submission_institution_monthly_institution_id');
$table->integer('month');
$table->integer('metric_investigations');
$table->integer('metric_investigations_unique');
$table->integer('metric_requests');
$table->integer('metric_requests_unique');
$table->index(['context_id', 'submission_id'], 'msim_context_id_submission_id');
$table->unique(['context_id', 'submission_id', 'institution_id', 'month'], 'msim_uc_context_id_submission_id_institution_id_month');
});
Schema::create('metrics_submission_geo_daily', function (Blueprint $table) {
$table->bigIncrements('metrics_submission_geo_daily_id');
$table->string('load_id', 255);
$table->index(['load_id'], 'msgd_load_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msgd_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_geo_daily_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msgd_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_geo_daily_submission_id');
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->date('date');
$table->integer('metric');
$table->integer('metric_unique');
$table->index(['context_id', 'submission_id'], 'msgd_context_id_submission_id');
$table->unique(['load_id', 'context_id', 'submission_id', 'country', 'region', 'city', 'date'], 'msgd_uc_load_context_submission_c_r_c_date');
});
Schema::create('metrics_submission_geo_monthly', function (Blueprint $table) {
$table->bigIncrements('metrics_submission_geo_monthly_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'msgm_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'metrics_submission_geo_monthly_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'msgm_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'metrics_submission_geo_monthly_submission_id');
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->integer('month');
$table->integer('metric');
$table->integer('metric_unique');
$table->index(['context_id', 'submission_id'], 'msgm_context_id_submission_id');
$table->unique(['context_id', 'submission_id', 'country', 'region', 'city', 'month'], 'msgm_uc_context_submission_c_r_c_month');
});
// Usage stats total item temporary records
Schema::create('usage_stats_total_temporary_records', function (Blueprint $table) {
$table->bigIncrements('usage_stats_temp_total_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 255);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->string('canonical_url', 255);
$table->bigInteger('issue_id')->nullable();
$table->foreign('issue_id', 'ust_issue_id_foreign')->references('issue_id')->on('issues')->onDelete('cascade');
$table->index(['issue_id'], 'usage_stats_total_temporary_records_issue_id');
$table->bigInteger('issue_galley_id')->nullable();
$table->foreign('issue_galley_id', 'ust_issue_galley_id_foreign')->references('galley_id')->on('issue_galleys')->onDelete('cascade');
$table->index(['issue_galley_id'], 'usage_stats_total_temporary_records_issue_galley_id');
$table->bigInteger('context_id');
$table->foreign('context_id', 'ust_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usage_stats_total_temporary_records_context_id');
$table->bigInteger('submission_id')->nullable();
$table->foreign('submission_id', 'ust_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usage_stats_total_temporary_records_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'ust_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usage_stats_total_temporary_records_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'ust_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usage_stats_total_temporary_records_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 255);
});
// Usage stats unique item investigations temporary records
// No need to consider issue_id and issue_galley_id here because
// investigations are only relevant/calculated on submission level.
Schema::create('usage_stats_unique_item_investigations_temporary_records', function (Blueprint $table) {
$table->bigIncrements('usage_stats_temp_unique_item_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 255);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->bigInteger('context_id');
$table->foreign('context_id', 'usii_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usii_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'usii_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usii_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'usii_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usii_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'usii_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usii_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 255);
});
// Usage stats unique item requests temporary records
// No need to consider issue_id and issue_galley_id here because
// requests are only relevant/calculated on submission level.
Schema::create('usage_stats_unique_item_requests_temporary_records', function (Blueprint $table) {
$table->bigIncrements('usage_stats_temp_item_id');
$table->dateTime('date', $precision = 0);
$table->string('ip', 255);
$table->string('user_agent', 255);
$table->bigInteger('line_number');
$table->bigInteger('context_id');
$table->foreign('context_id', 'usir_context_id_foreign')->references('journal_id')->on('journals')->onDelete('cascade');
$table->index(['context_id'], 'usir_context_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'usir_submission_id_foreign')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'usir_submission_id');
$table->bigInteger('representation_id')->nullable();
$table->foreign('representation_id', 'usir_representation_id_foreign')->references('galley_id')->on('publication_galleys')->onDelete('cascade');
$table->index(['representation_id'], 'usir_representation_id');
$table->bigInteger('submission_file_id')->unsigned()->nullable();
$table->foreign('submission_file_id', 'usir_submission_file_id_foreign')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'usir_submission_file_id');
$table->bigInteger('assoc_type');
$table->smallInteger('file_type')->nullable();
$table->string('country', 2)->default('');
$table->string('region', 3)->default('');
$table->string('city', 255)->default('');
$table->string('load_id', 255);
});
// Usage stats institution temporary records
// This table is needed because of data normalization
Schema::create('usage_stats_institution_temporary_records', function (Blueprint $table) {
$table->bigIncrements('usage_stats_temp_institution_id');
$table->string('load_id', 255);
$table->bigInteger('line_number');
$table->bigInteger('institution_id');
$table->foreign('institution_id', 'usi_institution_id_foreign')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'usi_institution_id');
$table->unique(['load_id', 'line_number', 'institution_id'], 'usitr_load_id_line_number_institution_id');
});
}
/**
* Reverse the migration
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,30 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_MetricsContext.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_MetricsContext
*
* @brief Migrate context stats data from the old DB table metrics into the new DB table metrics_context.
*/
namespace APP\migration\upgrade\v3_4_0;
class I6782_MetricsContext extends \PKP\migration\upgrade\v3_4_0\I6782_MetricsContext
{
private const ASSOC_TYPE_CONTEXT = 0x0000100;
protected function getMetricType(): string
{
return 'ojs::counter';
}
protected function getContextAssocType(): int
{
return self::ASSOC_TYPE_CONTEXT;
}
}
@@ -0,0 +1,23 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_MetricsGeo.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_MetricsGeo
*
* @brief Migrate submission stats Geo data from the old DB table metrics into the new DB table metrics_submission_geo_daily, then aggregate monthly.
*/
namespace APP\migration\upgrade\v3_4_0;
class I6782_MetricsGeo extends \PKP\migration\upgrade\v3_4_0\I6782_MetricsGeo
{
protected function getMetricType(): string
{
return 'ojs::counter';
}
}
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_MetricsIssue.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_MetricsIssue
*
* @brief Migrate issue stats data from the old DB table metrics into the new DB table metrics_issue.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I6782_MetricsIssue extends Migration
{
private const ASSOC_TYPE_ISSUE = 0x0000103;
private const ASSOC_TYPE_ISSUE_GALLEY = 0x0000105;
/**
* Run the migration.
*/
public function up(): void
{
$dayFormatSql = "DATE_FORMAT(STR_TO_DATE(m.day, '%Y%m%d'), '%Y-%m-%d')";
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
$dayFormatSql = "to_date(m.day, 'YYYYMMDD')";
}
// The not existing foreign keys should already be moved to the metrics_tmp in I6782_OrphanedMetrics
// Migrate issue metrics; consider issue TOCs and galley files
$selectIssueMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.context_id, m.assoc_id, null, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', self::ASSOC_TYPE_ISSUE)
->where('m.metric_type', '=', 'ojs::counter');
DB::table('metrics_issue')->insertUsing(['load_id', 'context_id', 'issue_id', 'issue_galley_id', 'date', 'metric'], $selectIssueMetrics);
$selectIssueGalleyMetrics = DB::table('metrics as m')
->join('issue_galleys as ig', 'ig.galley_id', '=', 'm.assoc_id')
->select(DB::raw("m.load_id, m.context_id, ig.issue_id, m.assoc_id, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', self::ASSOC_TYPE_ISSUE_GALLEY)
->where('m.metric_type', '=', 'ojs::counter');
DB::table('metrics_issue')->insertUsing(['load_id', 'context_id', 'issue_id', 'issue_galley_id', 'date', 'metric'], $selectIssueGalleyMetrics);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_MetricsSubmission.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_MetricsSubmission
*
* @brief Migrate submissions stats data from the old DB table metrics into the new DB table metrics_submission.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I6782_MetricsSubmission extends Migration
{
private const ASSOC_TYPE_SUBMISSION = 0x0100009;
private const ASSOC_TYPE_SUBMISSION_FILE = 0x0000203;
private const ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 0x0000213;
/**
* Run the migration.
*/
public function up(): void
{
$dayFormatSql = "DATE_FORMAT(STR_TO_DATE(m.day, '%Y%m%d'), '%Y-%m-%d')";
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
$dayFormatSql = "to_date(m.day, 'YYYYMMDD')";
}
// The not existing foreign keys should already be moved to the metrics_tmp in I6782_OrphanedMetrics
// Migrate submission metrics; consider abstracts, galley and supp files
$selectSubmissionMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.context_id, m.assoc_id, null, null, null, m.assoc_type, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION)
->where('m.metric_type', '=', 'ojs::counter');
DB::table('metrics_submission')->insertUsing(['load_id', 'context_id', 'submission_id', 'representation_id', 'submission_file_id', 'file_type', 'assoc_type', 'date', 'metric'], $selectSubmissionMetrics);
$selectSubmissionFileMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.context_id, m.submission_id, m.representation_id, m.assoc_id, m.file_type, m.assoc_type, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE)
->where('m.metric_type', '=', 'ojs::counter');
DB::table('metrics_submission')->insertUsing(['load_id', 'context_id', 'submission_id', 'representation_id', 'submission_file_id', 'file_type', 'assoc_type', 'date', 'metric'], $selectSubmissionFileMetrics);
$selectSubmissionSuppFileMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.context_id, m.submission_id, m.representation_id, m.assoc_id, m.file_type, m.assoc_type, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER)
->where('m.metric_type', '=', 'ojs::counter');
DB::table('metrics_submission')->insertUsing(['load_id', 'context_id', 'submission_id', 'representation_id', 'submission_file_id', 'file_type', 'assoc_type', 'date', 'metric'], $selectSubmissionSuppFileMetrics);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_OrphanedMetrics.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I6782_OrphanedMetrics
*
* @brief Migrate metrics data from objects that do not exist any more and from assoc types that are not considered in the upgrade into the temporary table.
* These entries will be copied back and stay in the table metrics_old, s. I6782_CleanOldMetrics.
* Consider only metric_type ojs::counter here, because these entries will be removed during the upgrade.
*/
namespace APP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class I6782_OrphanedMetrics extends \PKP\migration\upgrade\v3_4_0\I6782_OrphanedMetrics
{
private const ASSOC_TYPE_CONTEXT = 0x0000100;
private const ASSOC_TYPE_ISSUE = 0x0000103;
private const ASSOC_TYPE_ISSUE_GALLEY = 0x0000105;
protected function getMetricType(): string
{
return 'ojs::counter';
}
protected function getContextAssocType(): int
{
return self::ASSOC_TYPE_CONTEXT;
}
protected function getContextTable(): string
{
return 'journals';
}
protected function getContextKeyField(): string
{
return 'journal_id';
}
protected function getRepresentationTable(): string
{
return 'publication_galleys';
}
protected function getRepresentationKeyField(): string
{
return 'galley_id';
}
protected function getAssocTypesToMigrate(): array
{
return array_merge(
[
self::ASSOC_TYPE_CONTEXT,
self::ASSOC_TYPE_ISSUE,
self::ASSOC_TYPE_ISSUE_GALLEY,
],
parent::getAssocTypesToMigrate()
);
}
/**
* Run the migration.
*
* assoc_object_type, assoc_object_id, and pkp_section_id will not be considered here, because they are not relevant for the migration
*/
public function up(): void
{
parent::up();
$metricsColumns = Schema::getColumnListing('metrics_tmp');
// Metrics issue IDs
// as m.assoc_id
$orphanedIds = DB::table('metrics AS m')->leftJoin('issues AS i', 'm.assoc_id', '=', 'i.issue_id')->where('m.assoc_type', '=', self::ASSOC_TYPE_ISSUE)->whereNull('i.issue_id')->distinct()->pluck('m.assoc_id');
$orphandedIssues = DB::table('metrics')->select($metricsColumns)->where('assoc_type', '=', self::ASSOC_TYPE_ISSUE)->whereIn('assoc_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedIssues);
DB::table('metrics')->where('assoc_type', '=', self::ASSOC_TYPE_ISSUE)->whereIn('assoc_id', $orphanedIds)->delete();
// Clean orphaned metrics issue galley IDs
$orphanedIds = DB::table('metrics AS m')->leftJoin('issue_galleys AS ig', 'm.assoc_id', '=', 'ig.galley_id')->where('m.assoc_type', '=', self::ASSOC_TYPE_ISSUE_GALLEY)->whereNull('ig.galley_id')->distinct()->pluck('m.assoc_id');
$orphandedIssuesGalleys = DB::table('metrics')->select($metricsColumns)->where('assoc_type', '=', self::ASSOC_TYPE_ISSUE_GALLEY)->whereIn('assoc_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedIssuesGalleys);
DB::table('metrics')->where('assoc_type', '=', self::ASSOC_TYPE_ISSUE_GALLEY)->whereIn('assoc_id', $orphanedIds)->delete();
}
}

Some files were not shown because too many files have changed in this diff Show More