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
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* @file classes/migration/Migration.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 Migration
*
* @brief Base class for PKP migrations.
*/
namespace PKP\migration;
use PKP\install\Installer;
abstract class Migration extends \Illuminate\Database\Migrations\Migration
{
protected array $_attributes;
protected Installer $_installer;
/**
* Constructor
*/
public function __construct(Installer $installer, array $attributes)
{
$this->_attributes = $attributes;
$this->_installer = $installer;
}
/**
* Run the migrations.
*/
abstract public function up(): void;
/**
* Reverse the migration.
*/
abstract public function down(): void;
}
@@ -0,0 +1,97 @@
<?php
/**
* @file classes/migration/install/AnnouncementsMigration.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 AnnouncementsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema as Schema;
class AnnouncementsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Announcement types.
Schema::create('announcement_types', function (Blueprint $table) {
$table->comment('Announcement types allow for announcements to optionally be categorized.');
$table->bigInteger('type_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'announcement_types_context_id');
});
// Locale-specific announcement type data
Schema::create('announcement_type_settings', function (Blueprint $table) {
$table->comment('More data about announcement types, including localized properties like their names.');
$table->bigIncrements('announcement_type_setting_id');
$table->bigInteger('type_id');
$table->foreign('type_id')->references('type_id')->on('announcement_types')->onDelete('cascade');
$table->index(['type_id'], 'announcement_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'], 'announcement_type_settings_unique');
});
// Announcements.
Schema::create('announcements', function (Blueprint $table) {
$table->comment('Announcements are messages that can be presented to users e.g. on the homepage.');
$table->bigInteger('announcement_id')->autoIncrement();
// NOT NULL not included for upgrade purposes
$table->smallInteger('assoc_type')->nullable();
$table->bigInteger('assoc_id');
$table->bigInteger('type_id')->nullable();
$table->foreign('type_id')->references('type_id')->on('announcement_types')->onDelete('set null');
$table->index(['type_id'], 'announcements_type_id');
$table->date('date_expire')->nullable();
$table->datetime('date_posted');
$table->index(['assoc_type', 'assoc_id'], 'announcements_assoc');
});
// Locale-specific announcement data
Schema::create('announcement_settings', function (Blueprint $table) {
$table->comment('More data about announcements, including localized properties like names and contents.');
$table->bigIncrements('announcement_setting_id');
$table->bigInteger('announcement_id');
$table->foreign('announcement_id')->references('announcement_id')->on('announcements')->onDelete('cascade');
$table->index(['announcement_id'], 'announcement_settings_announcement_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['announcement_id', 'locale', 'setting_name'], 'announcement_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('announcement_settings');
Schema::drop('announcements');
Schema::drop('announcement_type_settings');
Schema::drop('announcement_types');
}
}
@@ -0,0 +1,90 @@
<?php
/**
* @file classes/migration/install/CategoriesMigration.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 CategoriesMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CategoriesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->comment('Categories permit the organization of submissions into a heirarchical structure.');
$table->bigInteger('category_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'category_context_id');
$table->bigInteger('parent_id')->nullable(); // Self-referential foreign key set below
$table->bigInteger('seq')->nullable();
$table->string('path', 255);
$table->text('image')->nullable();
$table->index(['context_id', 'parent_id'], 'category_context_parent_id');
$table->unique(['context_id', 'path'], 'category_path');
});
Schema::table('categories', function (Blueprint $table) {
$table->foreign('parent_id')->references('category_id')->on('categories')->onDelete('set null');
$table->index(['parent_id'], 'category_parent_id');
});
// Category-specific settings
Schema::create('category_settings', function (Blueprint $table) {
$table->comment('More data about categories, including localized properties such as names.');
$table->bigIncrements('category_setting_id');
$table->bigInteger('category_id');
$table->foreign('category_id')->references('category_id')->on('categories')->onDelete('cascade');
$table->index(['category_id'], 'category_settings_category_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['category_id', 'locale', 'setting_name'], 'category_settings_unique');
});
// Associations for categories and publications.
Schema::create('publication_categories', function (Blueprint $table) {
$table->comment('Associates publications (and thus submissions) with categories.');
$table->bigIncrements('publication_category_id');
$table->bigInteger('publication_id');
$table->foreign('publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'publication_categories_publication_id');
$table->bigInteger('category_id');
$table->foreign('category_id')->references('category_id')->on('categories')->onDelete('cascade');
$table->index(['category_id'], 'publication_categories_category_id');
$table->unique(['publication_id', 'category_id'], 'publication_categories_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('categories');
Schema::drop('category_settings');
Schema::drop('publication_categories');
}
}
@@ -0,0 +1,302 @@
<?php
/**
* @file classes/migration/install/CommonMigration.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 CommonMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use APP\core\Application;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CommonMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('versions', function (Blueprint $table) {
$table->comment('Describes the installation and upgrade version history for the application and all installed plugins.');
$table->bigIncrements('version_id');
$table->integer('major')->default(0)->comment('Major component of version number, e.g. the 2 in OJS 2.3.8-0');
$table->integer('minor')->default(0)->comment('Minor component of version number, e.g. the 3 in OJS 2.3.8-0');
$table->integer('revision')->default(0)->comment('Revision component of version number, e.g. the 8 in OJS 2.3.8-0');
$table->integer('build')->default(0)->comment('Build component of version number, e.g. the 0 in OJS 2.3.8-0');
$table->datetime('date_installed');
$table->smallInteger('current')->default(0)->comment('1 iff the version entry being described is currently active. This permits the table to store past installation history for forensic purposes.');
$table->string('product_type', 30)->comment('Describes the type of product this row describes, e.g. "plugins.generic" (for a generic plugin) or "core" for the application itself')->nullable();
$table->string('product', 30)->comment('Uniquely identifies the product this version row describes, e.g. "ojs2" for OJS 2.x, "languageToggle" for the language toggle block plugin, etc.')->nullable();
$table->string('product_class_name', 80)->comment('Specifies the class name associated with this product, for plugins, or the empty string where not applicable.')->nullable();
$table->smallInteger('lazy_load')->default(0)->comment('1 iff the row describes a lazy-load plugin; 0 otherwise');
$table->smallInteger('sitewide')->default(0)->comment('1 iff the row describes a site-wide plugin; 0 otherwise');
$table->unique(['product_type', 'product', 'major', 'minor', 'revision', 'build'], 'versions_unique');
});
// Common site settings.
Schema::create('site', function (Blueprint $table) {
$table->comment('A singleton table describing basic information about the site.');
$table->bigIncrements('site_id');
$table->bigInteger('redirect')->default(0)->comment('If not 0, redirect to the specified journal/conference/... site.');
$table->string('primary_locale', 14)->comment('Primary locale for the site.');
$table->smallInteger('min_password_length')->default(6);
$table->string('installed_locales', 1024)->default('en')->comment('Locales for which support has been installed.');
$table->string('supported_locales', 1024)->comment('Locales supported by the site (for hosted journals/conferences/...).')->nullable();
$table->string('original_style_file_name', 255)->nullable();
});
// Site settings.
Schema::create('site_settings', function (Blueprint $table) {
$table->comment('More data about the site, including localized properties such as its name.');
$table->bigIncrements('site_setting_id');
$table->string('setting_name', 255);
$table->string('locale', 14)->default('');
$table->mediumText('setting_value')->nullable();
$table->unique(['setting_name', 'locale'], 'site_settings_unique');
});
Schema::create('users', function (Blueprint $table) {
$table->comment('All registered users, including authentication data and profile data.');
$table->bigInteger('user_id')->autoIncrement();
$table->string('username', 32);
$table->string('password', 255);
$table->string('email', 255);
$table->string('url', 2047)->nullable();
$table->string('phone', 32)->nullable();
$table->string('mailing_address', 255)->nullable();
$table->string('billing_address', 255)->nullable();
$table->string('country', 90)->nullable();
$table->string('locales', 255)->default('[]');
$table->text('gossip')->nullable();
$table->datetime('date_last_email')->nullable();
$table->datetime('date_registered');
$table->datetime('date_validated')->nullable();
$table->datetime('date_last_login')->nullable();
$table->smallInteger('must_change_password')->nullable();
$table->bigInteger('auth_id')->nullable();
$table->string('auth_str', 255)->nullable();
$table->smallInteger('disabled')->default(0);
$table->text('disabled_reason')->nullable();
$table->smallInteger('inline_help')->nullable();
});
switch (DB::getDriverName()) {
case 'mysql':
Schema::table('users', function (Blueprint $table) {
$table->unique(['username'], 'users_username');
$table->unique(['email'], 'users_email');
});
break;
case 'pgsql':
DB::unprepared('CREATE UNIQUE INDEX users_username on users (LOWER(username));');
DB::unprepared('CREATE UNIQUE INDEX users_email on users (LOWER(email));');
break;
}
Schema::create('user_settings', function (Blueprint $table) {
$table->comment('More data about users, including localized properties like their name and affiliation.');
$table->bigIncrements('user_setting_id');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'user_settings_user_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['user_id', 'locale', 'setting_name'], 'user_settings_unique');
$table->index(['setting_name', 'locale'], 'user_settings_locale_setting_name_index');
});
Schema::create('sessions', function (Blueprint $table) {
$table->comment('Session data for logged-in users.');
$table->string('session_id', 128);
$table->bigInteger('user_id')->nullable();
$table->foreign('user_id', 'sessions_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'sessions_user_id');
$table->string('ip_address', 39);
$table->string('user_agent', 255)->nullable();
$table->bigInteger('created')->default(0);
$table->bigInteger('last_used')->default(0);
$table->smallInteger('remember')->default(0);
$table->text('data');
$table->string('domain', 255)->nullable();
$table->unique(['session_id'], 'sessions_pkey');
});
Schema::create('access_keys', function (Blueprint $table) {
$table->comment('Access keys are used to provide pseudo-login functionality for security-minimal tasks. Passkeys can be emailed directly to users, who can use them for a limited time in lieu of standard username and password.');
$table->bigInteger('access_key_id')->autoIncrement();
$table->string('context', 40);
$table->string('key_hash', 40);
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'access_keys_user_id');
$table->bigInteger('assoc_id')->nullable();
$table->datetime('expiry_date');
$table->index(['key_hash', 'user_id', 'context'], 'access_keys_hash');
});
Schema::create('notifications', function (Blueprint $table) {
$table->comment('User notifications created during certain operations.');
$table->bigInteger('notification_id')->autoIncrement();
$table->bigInteger('context_id')->nullable();
$contextDao = Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'notifications_context_id');
$table->bigInteger('user_id')->nullable();
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notifications_user_id');
$table->bigInteger('level');
$table->bigInteger('type');
$table->datetime('date_created');
$table->datetime('date_read')->nullable();
$table->bigInteger('assoc_type')->nullable();
$table->bigInteger('assoc_id')->nullable();
$table->index(['context_id', 'user_id', 'level'], 'notifications_context_id_user_id');
$table->index(['context_id', 'level'], 'notifications_context_id_level');
$table->index(['assoc_type', 'assoc_id'], 'notifications_assoc');
$table->index(['user_id', 'level'], 'notifications_user_id_level');
});
// Stores metadata for specific notifications
Schema::create('notification_settings', function (Blueprint $table) {
$table->comment('More data about notifications, including localized properties.');
$table->bigIncrements('notification_setting_id');
$table->bigInteger('notification_id');
$table->foreign('notification_id')->references('notification_id')->on('notifications')->onDelete('cascade');
$table->index(['notification_id'], 'notification_settings_notification_id');
$table->string('locale', 14)->nullable();
$table->string('setting_name', 64);
$table->mediumText('setting_value');
$table->string('setting_type', 6)->comment('(bool|int|float|string|object)');
$table->unique(['notification_id', 'locale', 'setting_name'], 'notification_settings_unique');
});
Schema::create('notification_subscription_settings', function (Blueprint $table) {
$table->comment('Which email notifications a user has chosen to unsubscribe from.');
$table->bigInteger('setting_id')->autoIncrement();
$table->string('setting_name', 64);
$table->mediumText('setting_value');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notification_subscription_settings_user_id');
$table->bigInteger('context');
$contextDao = Application::getContextDAO();
$table->foreign('context')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context'], 'notification_subscription_settings_context');
$table->string('setting_type', 6)->comment('(bool|int|float|string|object)');
});
Schema::create('email_templates_default_data', function (Blueprint $table) {
$table->comment('Default email templates created for every installed locale.');
$table->bigIncrements('email_templates_default_data_id');
$table->string('email_key', 255)->comment('Unique identifier for this email.');
$table->string('locale', 14)->default('en');
$table->string('name', 255);
$table->string('subject', 255);
$table->text('body')->nullable();
$table->unique(['email_key', 'locale'], 'email_templates_default_data_unique');
});
Schema::create('email_templates', function (Blueprint $table) {
$table->comment('Custom email templates created by each context, and overrides of the default templates.');
$table->bigInteger('email_id')->autoIncrement();
$table->string('email_key', 255)->comment('Unique identifier for this email.');
$table->bigInteger('context_id');
$contextDao = Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'email_templates_context_id');
$table->string('alternate_to', 255)->nullable();
$table->index(['alternate_to'], 'email_templates_alternate_to');
$table->unique(['email_key', 'context_id'], 'email_templates_email_key');
});
Schema::create('email_templates_settings', function (Blueprint $table) {
$table->comment('More data about custom email templates, including localized properties such as the subject and body.');
$table->bigIncrements('email_template_setting_id');
$table->bigInteger('email_id');
$table->foreign('email_id', 'email_templates_settings_email_id')->references('email_id')->on('email_templates')->onDelete('cascade');
$table->index(['email_id'], 'email_templates_settings_email_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['email_id', 'locale', 'setting_name'], 'email_templates_settings_unique');
});
// Resumption tokens for the OAI protocol interface.
Schema::create('oai_resumption_tokens', function (Blueprint $table) {
$table->comment('OAI resumption tokens are used to allow for pagination of large result sets into manageable pieces.');
$table->bigIncrements('oai_resumption_token_id');
$table->string('token', 32);
$table->bigInteger('expire');
$table->integer('record_offset');
$table->text('params')->nullable();
$table->unique(['token'], 'oai_resumption_tokens_unique');
});
// Plugin settings.
Schema::create('plugin_settings', function (Blueprint $table) {
$table->comment('More data about plugins, including localized properties. This table is frequently used to store plugin-specific configuration.');
$table->bigIncrements('plugin_setting_id');
$table->string('plugin_name', 80);
$table->bigInteger('context_id');
$table->string('setting_name', 80);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6)->comment('(bool|int|float|string|object)');
$table->index(['plugin_name'], 'plugin_settings_plugin_name');
$table->unique(['plugin_name', 'context_id', 'setting_name'], 'plugin_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('plugin_settings');
Schema::drop('oai_resumption_tokens');
Schema::drop('email_templates_settings');
Schema::drop('email_templates');
Schema::drop('email_templates_default_data');
Schema::drop('mailable_templates');
Schema::drop('notification_subscription_settings');
Schema::drop('notification_settings');
Schema::drop('notifications');
Schema::drop('access_keys');
Schema::drop('sessions');
Schema::drop('user_settings');
Schema::drop('users');
Schema::drop('site_settings');
Schema::drop('site');
Schema::drop('versions');
}
}
@@ -0,0 +1,91 @@
<?php
/**
* @file classes/migration/install/ControlledVocabMigration.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 ControlledVocabMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ControlledVocabMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Controlled vocabularies
Schema::create('controlled_vocabs', function (Blueprint $table) {
$table->comment('Every word or phrase used in a controlled vocabulary. Controlled vocabularies are used for submission metadata like keywords and subjects, reviewer interests, and wherever a similar dictionary of words or phrases is required. Each entry corresponds to a word or phrase like "cellular reproduction" and a type like "submissionKeyword".');
$table->bigInteger('controlled_vocab_id')->autoIncrement();
$table->string('symbolic', 64);
$table->bigInteger('assoc_type')->default(0);
$table->bigInteger('assoc_id')->default(0);
$table->unique(['symbolic', 'assoc_type', 'assoc_id'], 'controlled_vocab_symbolic');
});
// Controlled vocabulary entries
Schema::create('controlled_vocab_entries', function (Blueprint $table) {
$table->comment('The order that a word or phrase used in a controlled vocabulary should appear. For example, the order of keywords in a publication.');
$table->bigInteger('controlled_vocab_entry_id')->autoIncrement();
$table->bigInteger('controlled_vocab_id');
$table->foreign('controlled_vocab_id')->references('controlled_vocab_id')->on('controlled_vocabs')->onDelete('cascade');
$table->index(['controlled_vocab_id'], 'controlled_vocab_entries_controlled_vocab_id');
$table->float('seq', 8, 2)->nullable();
$table->index(['controlled_vocab_id', 'seq'], 'controlled_vocab_entries_cv_id');
});
// Controlled vocabulary entry settings
Schema::create('controlled_vocab_entry_settings', function (Blueprint $table) {
$table->comment('More data about a controlled vocabulary entry, including localized properties such as the actual word or phrase.');
$table->bigIncrements('controlled_vocab_entry_setting_id');
$table->bigInteger('controlled_vocab_entry_id');
$table->foreign('controlled_vocab_entry_id', 'c_v_e_s_entry_id')->references('controlled_vocab_entry_id')->on('controlled_vocab_entries')->onDelete('cascade');
$table->index(['controlled_vocab_entry_id'], 'c_v_e_s_entry_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['controlled_vocab_entry_id', 'locale', 'setting_name'], 'c_v_e_s_pkey');
});
// Reviewer Interests Associative Table
Schema::create('user_interests', function (Blueprint $table) {
$table->comment('Associates users with user interests (which are stored in the controlled vocabulary tables).');
$table->bigIncrements('user_interest_id');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'user_interests_user_id');
$table->bigInteger('controlled_vocab_entry_id');
$table->foreign('controlled_vocab_entry_id')->references('controlled_vocab_entry_id')->on('controlled_vocab_entries')->onDelete('cascade');
$table->index(['controlled_vocab_entry_id'], 'user_interests_controlled_vocab_entry_id');
$table->unique(['user_id', 'controlled_vocab_entry_id'], 'u_e_pkey');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('user_interests');
Schema::drop('controlled_vocab_entry_settings');
Schema::drop('controlled_vocab_entries');
Schema::drop('controlled_vocabs');
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @file classes/migration/install/DoiMigration.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 DoiMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class DoiMigration extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// NB: A foreign key constraint is placed on context_id, but this is application dependent and
// needs to be added once the context has been created.
// It will reference the app specific column (e.g. journal_id, press_id, etc.).
Schema::create('dois', function (Blueprint $table) {
$table->comment('Stores all DOIs used in the system.');
$table->bigInteger('doi_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'dois_context_id');
$table->string('doi');
$table->smallInteger('status')->default(1);
});
// Settings
Schema::create('doi_settings', function (Blueprint $table) {
$table->comment('More data about DOIs, including the registration agency.');
$table->bigIncrements('doi_setting_id');
$table->bigInteger('doi_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['doi_id', 'locale', 'setting_name'], 'doi_settings_unique');
$table->index(['doi_id'], 'doi_settings_doi_id');
$table->foreign('doi_id')->references('doi_id')->on('dois')->cascadeOnDelete();
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('dois');
Schema::drop('doi_settings');
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @file classes/migration/install/FailedJobsMigration.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 FailedJobsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FailedJobsMigration extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
// Schema matches https://github.com/illuminate/queue/blob/7.x/Console/stubs/failed_jobs.stub
Schema::create('failed_jobs', function (Blueprint $table) {
$table->comment('A log of all failed jobs.');
$table->bigIncrements('id');
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migration.
*/
public function down()
{
Schema::dropIfExists('failed_jobs');
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @file classes/migration install/FilesMigration.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 FilesMigration
*
* @brief Create the files database table
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FilesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('files', function (Blueprint $table) {
$table->comment('Records information in the database about files tracked by the system, linking them to the local filesystem.');
$table->bigIncrements('file_id');
$table->string('path', 255);
$table->string('mimetype', 255);
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('files');
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @file classes/migration/install/GenresMigration.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 GenresMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\submission\Genre;
class GenresMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// A context's submission file genres.
Schema::create('genres', function (Blueprint $table) {
$table->comment('The types of submission files configured for each context, such as Article Text, Data Set, Transcript, etc.');
$table->bigInteger('genre_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'genres_context_id');
$table->bigInteger('seq');
$table->smallInteger('enabled')->default(1);
$table->bigInteger('category')->default(Genre::GENRE_CATEGORY_DOCUMENT);
$table->smallInteger('dependent')->default(0);
$table->smallInteger('supplementary')->default(0);
$table->smallInteger('required')->default(0)->comment('Whether or not at least one file of this genre is required for a new submission.');
$table->string('entry_key', 30)->nullable();
});
// Genre settings
Schema::create('genre_settings', function (Blueprint $table) {
$table->comment('More data about file genres, including localized properties such as the genre name.');
$table->bigIncrements('genre_setting_id');
$table->bigInteger('genre_id');
$table->foreign('genre_id')->references('genre_id')->on('genres')->onDelete('cascade');
$table->index(['genre_id'], 'genre_settings_genre_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(['genre_id', 'locale', 'setting_name'], 'genre_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('genre_settings');
Schema::drop('genres');
}
}
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/migration/install/HighlightsMigration.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 HighlightsMigration
*
* @brief Describe database table structures for highlights
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema as Schema;
class HighlightsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('highlights', function (Blueprint $table) {
$table->comment('Highlights are featured items that can be presented to users, for example on the homepage.');
$table->bigInteger('highlight_id')->autoIncrement();
$table->bigInteger('context_id')->nullable();
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'highlights_context_id');
$table->bigInteger('sequence');
$table->string('url', 2047);
});
Schema::create('highlight_settings', function (Blueprint $table) {
$table->comment('More data about highlights, including localized properties like title and description.');
$table->bigIncrements('highlight_setting_id');
$table->bigInteger('highlight_id');
$table->foreign('highlight_id')->references('highlight_id')->on('highlights')->onDelete('cascade');
$table->index(['highlight_id'], 'highlight_settings_highlight_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['highlight_id', 'locale', 'setting_name'], 'highlight_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('highlight_settings');
Schema::drop('highlights');
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @file classes/migration/install/InstitutionsMigration.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 InstitutionsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use APP\core\Application;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema as Schema;
class InstitutionsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('institutions', function (Blueprint $table) {
$table->comment('Institutions for statistics and subscriptions.');
$table->bigInteger('institution_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'institutions_context_id');
$table->string('ror', 255)->nullable()->comment('ROR (Research Organization Registry) ID');
$table->softDeletes('deleted_at', 0);
});
Schema::create('institution_settings', function (Blueprint $table) {
$table->comment('More data about institutions, including localized properties like names.');
$table->bigIncrements('institution_setting_id');
$table->bigInteger('institution_id');
$table->foreign('institution_id')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'institution_settings_institution_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['institution_id', 'locale', 'setting_name'], 'institution_settings_unique');
});
// Institution IPs and IP ranges.
Schema::create('institution_ip', function (Blueprint $table) {
$table->comment('Records IP address ranges and associates them with institutions.');
$table->bigInteger('institution_ip_id')->autoIncrement();
$table->bigInteger('institution_id');
$table->foreign('institution_id')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'institution_ip_institution_id');
$table->string('ip_string', 40);
$table->bigInteger('ip_start');
$table->bigInteger('ip_end')->nullable();
$table->index(['ip_start'], 'institution_ip_start');
$table->index(['ip_end'], 'institution_ip_end');
});
if (Schema::hasTable('institutional_subscriptions') && Schema::hasColumn('institutional_subscriptions', 'institution_id')) {
Schema::table('institutional_subscriptions', function (Blueprint $table) {
$table->comment('Links institutional subscriptions with institutions.');
$table->foreign('institution_id')->references('institution_id')->on('institutions');
$table->index(['institution_id'], 'institutional_subscriptions_institution_ip');
});
}
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('institutional_subscriptions', function (Blueprint $table) {
$table->dropForeign('institution_id');
});
Schema::drop('institution_settings');
Schema::drop('institution_ip');
Schema::drop('institutions');
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @file classes/migration/install/JobsMigration.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 JobsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class JobsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->comment('All pending or in-progress jobs.');
$table->bigIncrements('id');
$table->string('queue');
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
$table->index(['queue', 'reserved_at']);
});
Schema::create('job_batches', function (Blueprint $table) {
$table->comment('Job batches allow jobs to be collected into groups for managed processing.');
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->text('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('jobs');
Schema::drop('job_batches');
}
}
@@ -0,0 +1,77 @@
<?php
/**
* @file classes/migration/install/LibraryFilesMigration.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 LibraryFilesMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class LibraryFilesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Library files for a context
Schema::create('library_files', function (Blueprint $table) {
$table->comment('Library files can be associated with the context (press/server/journal) or with individual submissions, and are typically forms, agreements, and other administrative documents that are not part of the scholarly content.');
$table->bigInteger('file_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id', 'library_files_context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'library_files_context_id');
$table->string('file_name', 255);
$table->string('original_file_name', 255);
$table->string('file_type', 255);
$table->bigInteger('file_size');
$table->smallInteger('type');
$table->datetime('date_uploaded');
$table->datetime('date_modified');
$table->bigInteger('submission_id')->nullable();
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'library_files_submission_id');
$table->smallInteger('public_access')->default(0)->nullable();
});
// Library file metadata.
Schema::create('library_file_settings', function (Blueprint $table) {
$table->comment('More data about library files, including localized content such as names.');
$table->bigIncrements('library_file_setting_id');
$table->bigInteger('file_id');
$table->foreign('file_id')->references('file_id')->on('library_files')->onDelete('cascade');
$table->index(['file_id'], 'library_file_settings_file_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|date)');
$table->unique(['file_id', 'locale', 'setting_name'], 'library_file_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('library_file_settings');
Schema::drop('library_files');
}
}
@@ -0,0 +1,110 @@
<?php
/**
* @file classes/migration/install/LogMigration.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 LogMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class LogMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('event_log', function (Blueprint $table) {
$table->comment('A log of all events related to an object like a submission.');
$table->bigInteger('log_id')->autoIncrement();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->bigInteger('user_id')->nullable()->comment('NULL if it\'s system or automated event');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'event_log_user_id');
$table->datetime('date_logged');
$table->bigInteger('event_type')->nullable();
$table->text('message')->nullable();
$table->boolean('is_translated')->nullable();
$table->index(['assoc_type', 'assoc_id'], 'event_log_assoc');
});
Schema::create('event_log_settings', function (Blueprint $table) {
$table->comment('Data about an event log entry. This data is commonly used to display information about an event to a user.');
$table->bigIncrements('event_log_setting_id');
$table->bigInteger('log_id');
$table->foreign('log_id', 'event_log_settings_log_id')->references('log_id')->on('event_log')->onDelete('cascade');
$table->index(['log_id'], 'event_log_settings_log_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['log_id', 'setting_name', 'locale'], 'event_log_settings_unique');
});
// Add partial index (DBMS-specific)
switch (DB::getDriverName()) {
case 'mysql': DB::unprepared('CREATE INDEX event_log_settings_name_value ON event_log_settings (setting_name(50), setting_value(150))');
break;
case 'pgsql': DB::unprepared("CREATE INDEX event_log_settings_name_value ON event_log_settings (setting_name, setting_value) WHERE setting_name IN ('fileId', 'submissionId')");
break;
}
Schema::create('email_log', function (Blueprint $table) {
$table->comment('A record of email messages that are sent in relation to an associated entity, such as a submission.');
$table->bigInteger('log_id')->autoIncrement();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->bigInteger('sender_id');
$table->datetime('date_sent');
$table->bigInteger('event_type')->nullable();
$table->string('from_address', 255)->nullable();
$table->text('recipients')->nullable();
$table->text('cc_recipients')->nullable();
$table->text('bcc_recipients')->nullable();
$table->string('subject', 255)->nullable();
$table->text('body')->nullable();
$table->index(['assoc_type', 'assoc_id'], 'email_log_assoc');
});
// Associations for email logs within a user.
Schema::create('email_log_users', function (Blueprint $table) {
$table->comment('A record of users associated with an email log entry.');
$table->bigIncrements('email_log_user_id');
$table->bigInteger('email_log_id');
$table->foreign('email_log_id')->references('log_id')->on('email_log')->onDelete('cascade');
$table->index(['email_log_id'], 'email_log_users_email_log_id');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'email_log_users_user_id');
$table->unique(['email_log_id', 'user_id'], 'email_log_user_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('email_log_users');
Schema::drop('email_log');
Schema::drop('event_log_settings');
Schema::drop('event_log');
}
}
@@ -0,0 +1,115 @@
<?php
/**
* @file classes/migration/install/MetadataMigration.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 MetadataMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MetadataMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Citations
Schema::create('citations', function (Blueprint $table) {
$table->comment('A citation made by an associated publication.');
$table->bigInteger('citation_id')->autoIncrement();
$table->bigInteger('publication_id');
$table->foreign('publication_id', 'citations_publication')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'citations_publication');
$table->text('raw_citation');
$table->bigInteger('seq')->default(0);
$table->unique(['publication_id', 'seq'], 'citations_publication_seq');
});
// Citation settings
Schema::create('citation_settings', function (Blueprint $table) {
$table->comment('Additional data about citations, including localized content.');
$table->bigIncrements('citation_setting_id');
$table->bigInteger('citation_id');
$table->foreign('citation_id', 'citation_settings_citation_id')->references('citation_id')->on('citations')->onDelete('cascade');
$table->index(['citation_id'], 'citation_settings_citation_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['citation_id', 'locale', 'setting_name'], 'citation_settings_unique');
});
// Filter groups
Schema::create('filter_groups', function (Blueprint $table) {
$table->comment('Filter groups are used to organized filters into named sets, which can be retrieved by the application for invocation.');
$table->bigInteger('filter_group_id')->autoIncrement();
$table->string('symbolic', 255)->nullable();
$table->string('display_name', 255)->nullable();
$table->string('description', 255)->nullable();
$table->string('input_type', 255)->nullable();
$table->string('output_type', 255)->nullable();
$table->unique(['symbolic'], 'filter_groups_symbolic');
});
// Configured filter instances (transformations)
Schema::create('filters', function (Blueprint $table) {
$table->comment('Filters represent a transformation of a supported piece of data from one form to another, such as a PHP object into an XML document.');
$table->bigInteger('filter_id')->autoIncrement();
$table->bigInteger('filter_group_id')->default(0);
$table->foreign('filter_group_id')->references('filter_group_id')->on('filter_groups')->onDelete('cascade');
$table->index(['filter_group_id'], 'filters_filter_group_id');
$table->bigInteger('context_id')->default(0);
$table->string('display_name', 255)->nullable();
$table->string('class_name', 255)->nullable();
$table->smallInteger('is_template')->default(0);
$table->bigInteger('parent_filter_id')->default(0);
$table->bigInteger('seq')->default(0);
});
// Filter Settings
Schema::create('filter_settings', function (Blueprint $table) {
$table->comment('More data about filters, including localized content.');
$table->bigIncrements('filter_setting_id');
$table->bigInteger('filter_id');
$table->foreign('filter_id')->references('filter_id')->on('filters')->onDelete('cascade');
$table->index(['filter_id'], 'filter_settings_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['filter_id', 'locale', 'setting_name'], 'filter_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('filter_settings');
Schema::drop('filters');
Schema::drop('filter_groups');
Schema::drop('citation_settings');
Schema::drop('citations');
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @file classes/migration/install/NavigationMenusMigration.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 NavigationMenusMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class NavigationMenusMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// NavigationMenus
Schema::create('navigation_menus', function (Blueprint $table) {
$table->comment('Navigation menus on the website are installed with the software as a default set, and can be customized.');
$table->bigInteger('navigation_menu_id')->autoIncrement();
$table->bigInteger('context_id');
$table->string('area_name', 255)->default('')->nullable();
$table->string('title', 255);
});
// NavigationMenuItems
Schema::create('navigation_menu_items', function (Blueprint $table) {
$table->comment('Navigation menu items are single elements within a navigation menu.');
$table->bigInteger('navigation_menu_item_id')->autoIncrement();
$table->bigInteger('context_id');
$table->string('path', 255)->default('')->nullable();
$table->string('type', 255)->default('')->nullable();
});
// Locale-specific navigation menu item data
Schema::create('navigation_menu_item_settings', function (Blueprint $table) {
$table->comment('More data about navigation menu items, including localized content such as names.');
$table->bigIncrements('navigation_menu_item_setting_id');
$table->bigInteger('navigation_menu_item_id');
$table->foreign('navigation_menu_item_id', 'navigation_menu_item_settings_navigation_menu_id')->references('navigation_menu_item_id')->on('navigation_menu_items')->onDelete('cascade');
$table->index(['navigation_menu_item_id'], 'navigation_menu_item_settings_navigation_menu_item_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->longText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['navigation_menu_item_id', 'locale', 'setting_name'], 'navigation_menu_item_settings_unique');
});
// NavigationMenuItemAssignments which assign menu items to a menu and describe nested menu structure.
Schema::create('navigation_menu_item_assignments', function (Blueprint $table) {
$table->comment('Links navigation menu items to navigation menus.');
$table->bigInteger('navigation_menu_item_assignment_id')->autoIncrement();
$table->bigInteger('navigation_menu_id');
$table->foreign('navigation_menu_id')->references('navigation_menu_id')->on('navigation_menus')->onDelete('cascade');
$table->index(['navigation_menu_id'], 'navigation_menu_item_assignments_navigation_menu_id');
$table->bigInteger('navigation_menu_item_id');
$table->foreign('navigation_menu_item_id')->references('navigation_menu_item_id')->on('navigation_menu_items')->onDelete('cascade');
$table->index(['navigation_menu_item_id'], 'navigation_menu_item_assignments_navigation_menu_item_id');
$table->bigInteger('parent_id')->nullable();
$table->bigInteger('seq')->default(0)->nullable();
});
// Locale-specific navigation menu item assignments data
Schema::create('navigation_menu_item_assignment_settings', function (Blueprint $table) {
$table->comment('More data about navigation menu item assignments to navigation menus, including localized content.');
$table->bigIncrements('navigation_menu_item_assignment_setting_id');
$table->bigInteger('navigation_menu_item_assignment_id');
$table->foreign('navigation_menu_item_assignment_id', 'assignment_settings_navigation_menu_item_assignment_id')->references('navigation_menu_item_assignment_id')->on('navigation_menu_item_assignments')->onDelete('cascade');
$table->index(['navigation_menu_item_assignment_id'], 'navigation_menu_item_assignment_settings_n_m_i_a_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['navigation_menu_item_assignment_id', 'locale', 'setting_name'], 'navigation_menu_item_assignment_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('navigation_menu_item_assignment_settings');
Schema::drop('navigation_menu_item_assignments');
Schema::drop('navigation_menu_item_settings');
Schema::drop('navigation_menu_items');
Schema::drop('navigation_menus');
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @file classes/migration/install/NotesMigration.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 NotesMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class NotesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notes', function (Blueprint $table) {
$table->comment('Notes allow users to annotate associated entities, such as submissions.');
$table->bigInteger('note_id')->autoIncrement();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notes_user_id');
$table->datetime('date_created');
$table->datetime('date_modified')->nullable();
$table->string('title', 255)->nullable();
$table->text('contents')->nullable();
$table->index(['assoc_type', 'assoc_id'], 'notes_assoc');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('notes');
}
}
@@ -0,0 +1,103 @@
<?php
/**
* @file classes/migration/install/ReviewFormsMigration.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 ReviewFormsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ReviewFormsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Review forms.
if (!Schema::hasTable('review_forms')) {
Schema::create('review_forms', function (Blueprint $table) {
$table->comment('Review forms provide custom templates for peer reviews with several types of questions.');
$table->bigInteger('review_form_id')->autoIncrement();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->float('seq', 8, 2)->nullable();
$table->smallInteger('is_active')->nullable();
});
}
// Review form settings
if (!Schema::hasTable('review_form_settings')) {
Schema::create('review_form_settings', function (Blueprint $table) {
$table->comment('More data about review forms, including localized content such as names.');
$table->bigIncrements('review_form_setting_id');
$table->bigInteger('review_form_id');
$table->foreign('review_form_id', 'review_form_settings_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('cascade');
$table->index(['review_form_id'], 'review_form_settings_review_form_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['review_form_id', 'locale', 'setting_name'], 'review_form_settings_unique');
});
}
// Review form elements.
if (!Schema::hasTable('review_form_elements')) {
Schema::create('review_form_elements', function (Blueprint $table) {
$table->comment('Each review form element represents a single question on a review form.');
$table->bigInteger('review_form_element_id')->autoIncrement();
$table->bigInteger('review_form_id');
$table->foreign('review_form_id', 'review_form_elements_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('cascade');
$table->index(['review_form_id'], 'review_form_elements_review_form_id');
$table->float('seq', 8, 2)->nullable();
$table->bigInteger('element_type')->nullable();
$table->smallInteger('required')->nullable();
$table->smallInteger('included')->nullable();
});
}
// Review form element settings
if (!Schema::hasTable('review_form_element_settings')) {
Schema::create('review_form_element_settings', function (Blueprint $table) {
$table->comment('More data about review form elements, including localized content such as question text.');
$table->bigIncrements('review_form_element_setting_id');
$table->bigInteger('review_form_element_id');
$table->foreign('review_form_element_id', 'review_form_element_settings_review_form_element_id')->references('review_form_element_id')->on('review_form_elements')->onDelete('cascade');
$table->index(['review_form_element_id'], 'review_form_element_settings_review_form_element_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->string('setting_type', 6);
$table->unique(['review_form_element_id', 'locale', 'setting_name'], 'review_form_element_settings_unique');
});
}
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('review_form_element_settings');
Schema::drop('review_form_elements');
Schema::drop('review_form_settings');
Schema::drop('review_forms');
}
}
@@ -0,0 +1,172 @@
<?php
/**
* @file classes/migration/install/ReviewsMigration.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 ReviewsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ReviewsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Review rounds.
Schema::create('review_rounds', function (Blueprint $table) {
$table->comment('Peer review assignments are organized into multiple rounds on a submission.');
$table->bigInteger('review_round_id')->autoIncrement();
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions');
$table->index(['submission_id'], 'review_rounds_submission_id');
$table->bigInteger('stage_id')->nullable();
$table->smallInteger('round');
$table->bigInteger('review_revision')->nullable();
$table->bigInteger('status')->nullable();
$table->unique(['submission_id', 'stage_id', 'round'], 'review_rounds_submission_id_stage_id_round_pkey');
});
Schema::table('edit_decisions', function (Blueprint $table) {
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds')->onDelete('cascade');
$table->index(['review_round_id'], 'edit_decisions_review_round_id');
});
// Reviewing assignments.
Schema::create('review_assignments', function (Blueprint $table) {
$table->comment('Data about peer review assignments for all submissions.');
$table->bigInteger('review_id')->autoIncrement();
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions');
$table->index(['submission_id'], 'review_assignments_submission_id');
$table->bigInteger('reviewer_id');
$table->foreign('reviewer_id')->references('user_id')->on('users');
$table->index(['reviewer_id'], 'review_assignments_reviewer_id');
$table->text('competing_interests')->nullable();
$table->smallInteger('recommendation')->nullable();
$table->datetime('date_assigned')->nullable();
$table->datetime('date_notified')->nullable();
$table->datetime('date_confirmed')->nullable();
$table->datetime('date_completed')->nullable();
$table->datetime('date_acknowledged')->nullable();
$table->datetime('date_due')->nullable();
$table->datetime('date_response_due')->nullable();
$table->datetime('last_modified')->nullable();
$table->smallInteger('reminder_was_automatic')->default(0);
$table->smallInteger('declined')->default(0);
$table->smallInteger('cancelled')->default(0);
$table->datetime('date_rated')->nullable();
$table->datetime('date_reminded')->nullable();
$table->smallInteger('quality')->nullable();
$table->bigInteger('review_round_id');
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds');
$table->index(['review_round_id', 'reviewer_id'], 'review_assignment_reviewer_round');
$table->smallInteger('stage_id');
$table->smallInteger('review_method')->default(\PKP\submission\reviewAssignment\ReviewAssignment::SUBMISSION_REVIEW_METHOD_ANONYMOUS);
$table->smallInteger('round')->default(1);
$table->smallInteger('step')->default(1);
$table->bigInteger('review_form_id')->nullable();
$table->foreign('review_form_id')->references('review_form_id')->on('review_forms');
$table->index(['review_form_id'], 'review_assignments_form_id');
$table->smallInteger('considered')->nullable();
$table->smallInteger('request_resent')->default(0);
// Normally reviewer can't be assigned twice on the same review round.
// HOWEVER, if two reviewer user accounts are subsequently merged, both will keep
// separate review assignments but the reviewer_id will become the same!
// (https://github.com/pkp/pkp-lib/issues/7678)
$table->index(['reviewer_id', 'review_id'], 'review_assignments_reviewer_review');
});
// Review form responses.
if (!Schema::hasTable('review_form_responses')) {
Schema::create('review_form_responses', function (Blueprint $table) {
$table->comment('Each review form response records a reviewer\'s answer to a review form element associated with a peer review.');
$table->bigIncrements('review_form_response_id');
$table->bigInteger('review_form_element_id');
$table->foreign('review_form_element_id')->references('review_form_element_id')->on('review_form_elements')->onDelete('cascade');
$table->index(['review_form_element_id'], 'review_form_responses_review_form_element_id');
$table->bigInteger('review_id');
$table->foreign('review_id')->references('review_id')->on('review_assignments')->onDelete('cascade');
$table->index(['review_id'], 'review_form_responses_review_id');
$table->string('response_type', 6)->nullable();
$table->text('response_value')->nullable();
$table->index(['review_form_element_id', 'review_id'], 'review_form_responses_unique');
});
}
// Submission Files for each review round
Schema::create('review_round_files', function (Blueprint $table) {
$table->comment('Records the files made available to reviewers for a round of reviews. These can be further customized on a per review basis with review_files.');
$table->bigIncrements('review_round_file_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'review_round_files_submission_id');
$table->bigInteger('review_round_id');
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds')->onDelete('cascade');
$table->index(['review_round_id'], 'review_round_files_review_round_id');
$table->smallInteger('stage_id');
$table->bigInteger('submission_file_id')->nullable(false)->unsigned();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'review_round_files_submission_file_id');
$table->unique(['submission_id', 'review_round_id', 'submission_file_id'], 'review_round_files_unique');
});
// Associates reviewable submission files with reviews
Schema::create('review_files', function (Blueprint $table) {
$table->comment('A list of the submission files made available to each assigned reviewer.');
$table->bigIncrements('review_file_id');
$table->bigInteger('review_id');
$table->foreign('review_id')->references('review_id')->on('review_assignments')->onDelete('cascade');
$table->index(['review_id'], 'review_files_review_id');
$table->bigInteger('submission_file_id')->nullable(false)->unsigned();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'review_files_submission_file_id');
$table->unique(['review_id', 'submission_file_id'], 'review_files_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('review_form_responses');
Schema::drop('review_assignments');
Schema::drop('review_files');
Schema::drop('review_round_files');
Schema::drop('review_rounds');
}
}
@@ -0,0 +1,121 @@
<?php
/**
* @file classes/migration/install/RolesAndUserGroupsMigration.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 RolesAndUserGroupsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use APP\core\Application;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RolesAndUserGroupsMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_groups', function (Blueprint $table) {
$table->comment('All defined user roles in a context, such as Author, Reviewer, Section Editor and Journal Manager.');
$table->bigInteger('user_group_id')->autoIncrement();
$table->bigInteger('context_id');
$table->bigInteger('role_id');
$table->smallInteger('is_default')->default(0);
$table->smallInteger('show_title')->default(1);
$table->smallInteger('permit_self_registration')->default(0);
$table->smallInteger('permit_metadata_edit')->default(0);
$table->index(['user_group_id'], 'user_groups_user_group_id');
$table->index(['context_id'], 'user_groups_context_id');
$table->index(['role_id'], 'user_groups_role_id');
});
Schema::create('user_group_settings', function (Blueprint $table) {
$table->comment('More data about user groups, including localized properties such as the name.');
$table->bigIncrements('user_group_setting_id');
$table->bigInteger('user_group_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['user_group_id', 'locale', 'setting_name'], 'user_group_settings_unique');
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'user_group_settings_user_group_id');
});
Schema::create('user_user_groups', function (Blueprint $table) {
$table->comment('Maps users to their assigned user_groups.');
$table->bigIncrements('user_user_group_id');
$table->bigInteger('user_group_id');
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'user_user_groups_user_group_id');
$table->bigInteger('user_id');
$table->foreign('user_id', 'user_user_groups_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'user_user_groups_user_id');
$table->unique(['user_group_id', 'user_id'], 'user_user_groups_unique');
});
Schema::create('user_group_stage', function (Blueprint $table) {
$table->comment('Which stages of the editorial workflow the user_groups can access.');
$table->bigIncrements('user_group_stage_id');
$table->bigInteger('context_id');
$contextDao = Application::getContextDAO();
$table->foreign('context_id', 'user_group_stage_context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'user_group_stage_context_id');
$table->bigInteger('user_group_id');
$table->foreign('user_group_id', 'user_group_stage_user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'user_group_stage_user_group_id');
$table->bigInteger('stage_id');
$table->index(['stage_id'], 'user_group_stage_stage_id');
$table->unique(['context_id', 'user_group_id', 'stage_id'], 'user_group_stage_unique');
});
Schema::create('stage_assignments', function (Blueprint $table) {
$table->comment('Who can access a submission while it is in the editorial workflow. Includes all editorial and author assignments. For reviewers, see review_assignments.');
$table->bigInteger('stage_assignment_id')->autoIncrement();
// The foreign key for this column is declared with the submissions table.
$table->bigInteger('submission_id');
$table->bigInteger('user_group_id');
$table->foreign('user_group_id', 'stage_assignments_user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'stage_assignments_user_group_id');
$table->bigInteger('user_id');
$table->foreign('user_id', 'stage_assignments_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'stage_assignments_user_id');
$table->datetime('date_assigned');
$table->smallInteger('recommend_only')->default(0);
$table->smallInteger('can_change_metadata')->default(0);
$table->unique(['submission_id', 'user_group_id', 'user_id'], 'stage_assignment');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('stage_assignments');
Schema::drop('user_group_stage');
Schema::drop('user_user_groups');
Schema::drop('user_group_settings');
Schema::drop('user_groups');
}
}
@@ -0,0 +1,44 @@
<?php
/**
* @file classes/migration/install/ScheduledTasksMigration.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 ScheduledTasksMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ScheduledTasksMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// The last run times of all scheduled tasks.
Schema::create('scheduled_tasks', function (Blueprint $table) {
$table->comment('The last time each scheduled task was run.');
$table->bigIncrements('scheduled_task_id');
$table->string('class_name', 255);
$table->datetime('last_run')->nullable();
$table->unique(['class_name'], 'scheduled_tasks_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('scheduled_tasks');
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @file classes/migration/install/SubmissionFilesMigration.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 SubmissionFilesMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SubmissionFilesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('submission_files', function (Blueprint $table) {
$table->comment('All files associated with a submission, such as those uploaded during submission, as revisions, or by copyeditors or layout editors for production.');
$table->bigIncrements('submission_file_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'submission_files_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'submission_files_submission_id');
$table->bigInteger('file_id')->nullable(false)->unsigned();
$table->foreign('file_id')->references('file_id')->on('files')->onDelete('cascade');
$table->index(['file_id'], 'submission_files_file_id');
// FK declared below table (circular reference)
$table->bigInteger('source_submission_file_id')->unsigned()->nullable();
$table->bigInteger('genre_id')->nullable();
$table->foreign('genre_id')->references('genre_id')->on('genres')->onDelete('set null');
$table->index(['genre_id'], 'submission_files_genre_id');
$table->bigInteger('file_stage');
$table->string('direct_sales_price', 255)->nullable();
$table->string('sales_type', 255)->nullable();
$table->smallInteger('viewable')->nullable();
$table->datetime('created_at');
$table->datetime('updated_at');
$table->bigInteger('uploader_user_id')->nullable();
$table->foreign('uploader_user_id')->references('user_id')->on('users')->onDelete('set null');
$table->index(['uploader_user_id'], 'submission_files_uploader_user_id');
$table->bigInteger('assoc_type')->nullable();
$table->bigInteger('assoc_id')->nullable();
// pkp/pkp-lib#5804
$table->index(['file_stage', 'assoc_type', 'assoc_id'], 'submission_files_stage_assoc');
});
Schema::table('submission_files', function (Blueprint $table) {
$table->foreign('source_submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['source_submission_file_id'], 'submission_files_source_submission_file_id');
});
Schema::create('submission_file_settings', function (Blueprint $table) {
$table->comment('Localized data about submission files like published metadata.');
$table->bigIncrements('submission_file_setting_id');
$table->foreignId('submission_file_id');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'submission_file_settings_submission_file_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['submission_file_id', 'locale', 'setting_name'], 'submission_file_settings_unique');
});
// Submission file revisions
Schema::create('submission_file_revisions', function (Blueprint $table) {
$table->comment('Revisions map submission_file entries to files on the data store.');
$table->bigIncrements('revision_id');
$table->bigInteger('submission_file_id')->unsigned();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'submission_file_revisions_submission_file_id');
$table->bigInteger('file_id')->unsigned();
$table->foreign('file_id')->references('file_id')->on('files')->onDelete('cascade');
$table->index(['file_id'], 'submission_file_revisions_file_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('submission_file_revisions');
Schema::drop('submission_file_settings');
Schema::drop('submission_files');
}
}
@@ -0,0 +1,288 @@
<?php
/**
* @file classes/migration/install/SubmissionsMigration.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 SubmissionsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\submission\PKPSubmission;
class SubmissionsMigration extends \PKP\migration\Migration
{
protected int $defaultStageId = WORKFLOW_STAGE_ID_SUBMISSION;
/**
* Run the migrations.
*/
public function up(): void
{
// Submissions
Schema::create('submissions', function (Blueprint $table) {
$table->comment('All submissions submitted to the context, including incomplete, declined and unpublished submissions.');
$table->bigInteger('submission_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id', 'submissions_context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'submissions_context_id');
// NOTE: The foreign key relationship on publications is declared where that table is created.
$table->bigInteger('current_publication_id')->nullable();
$table->datetime('date_last_activity')->nullable();
$table->datetime('date_submitted')->nullable();
$table->datetime('last_modified')->nullable();
$table->bigInteger('stage_id')->default($this->defaultStageId);
$table->string('locale', 14)->nullable();
$table->smallInteger('status')->default(PKPSubmission::STATUS_QUEUED);
$table->string('submission_progress', 50)->default('start');
// Used in OMP only; should not be null there
$table->smallInteger('work_type')->default(0)->nullable();
});
Schema::table('stage_assignments', function (Blueprint $table) {
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'stage_assignments_submission_id');
});
// Submission metadata
Schema::create('submission_settings', function (Blueprint $table) {
$table->comment('Localized data about submissions');
$table->bigIncrements('submission_setting_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'submission_settings_submission_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['submission_id', 'locale', 'setting_name'], 'submission_settings_unique');
});
// publication metadata
Schema::create('publication_settings', function (Blueprint $table) {
$table->comment('More data about publications, including localized properties such as the title and abstract.');
$table->bigIncrements('publication_setting_id');
// The foreign key relationship on this table is defined with the publications table.
$table->bigInteger('publication_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['publication_id', 'locale', 'setting_name'], 'publication_settings_unique');
});
// Add partial index (DBMS-specific)
switch (DB::getDriverName()) {
case 'mysql': DB::unprepared('CREATE INDEX publication_settings_name_value ON publication_settings (setting_name(50), setting_value(150))');
break;
case 'pgsql': DB::unprepared("CREATE INDEX publication_settings_name_value ON publication_settings (setting_name, setting_value) WHERE setting_name IN ('indexingState', 'medra::registeredDoi', 'datacite::registeredDoi', 'pub-id::publisher-id')");
break;
}
// Authors for submissions.
Schema::create('authors', function (Blueprint $table) {
$table->comment('The authors of a publication.');
$table->bigInteger('author_id')->autoIncrement();
$table->string('email', 90);
$table->smallInteger('include_in_browse')->default(1);
// The foreign key relationship on this table is defined with the publications table.
$table->bigInteger('publication_id');
$table->float('seq', 8, 2)->default(0);
$table->bigInteger('user_group_id')->nullable();
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'authors_user_group_id');
});
// Language dependent author metadata.
Schema::create('author_settings', function (Blueprint $table) {
$table->comment('More data about authors, including localized properties such as their name and affiliation.');
$table->bigIncrements('author_setting_id');
$table->bigInteger('author_id');
$table->foreign('author_id', 'author_settings_author_id')->references('author_id')->on('authors')->onDelete('cascade');
$table->index(['author_id'], 'author_settings_author_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['author_id', 'locale', 'setting_name'], 'author_settings_unique');
});
// Editor decisions.
Schema::create('edit_decisions', function (Blueprint $table) {
$table->comment('Editorial decisions recorded on a submission, such as decisions to accept or decline the submission, as well as decisions to send for review, send to copyediting, request revisions, and more.');
$table->bigInteger('edit_decision_id')->autoIncrement();
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'edit_decisions_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'edit_decisions_submission_id');
// Foreign key constraint is declared with review_rounds
$table->bigInteger('review_round_id')->nullable();
$table->bigInteger('stage_id')->nullable();
$table->smallInteger('round')->nullable();
$table->bigInteger('editor_id');
$table->foreign('editor_id', 'edit_decisions_editor_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['editor_id'], 'edit_decisions_editor_id');
$table->smallInteger('decision')->comment('A numeric constant indicating the decision that was taken. Possible values are listed in the Decision class.');
$table->datetime('date_decided');
});
// Comments posted on submissions
Schema::create('submission_comments', function (Blueprint $table) {
$table->comment('Comments on a submission, e.g. peer review comments');
$table->bigInteger('comment_id')->autoIncrement();
$table->bigInteger('comment_type')->nullable();
$table->bigInteger('role_id');
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'submission_comments_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'submission_comments_submission_id');
$table->bigInteger('assoc_id');
$table->bigInteger('author_id');
$table->foreign('author_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['author_id'], 'submission_comments_author_id');
$table->text('comment_title');
$table->text('comments')->nullable();
$table->datetime('date_posted')->nullable();
$table->datetime('date_modified')->nullable();
$table->smallInteger('viewable')->nullable();
});
// Assignments of sub editors to submission groups.
Schema::create('subeditor_submission_group', function (Blueprint $table) {
$table->comment('Subeditor assignments to e.g. sections and categories');
$table->bigIncrements('subeditor_submission_group_id');
$table->bigInteger('context_id');
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id', 'section_editors_context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'subeditor_submission_group_context_id');
$table->bigInteger('assoc_id');
$table->bigInteger('assoc_type');
$table->bigInteger('user_id');
$table->foreign('user_id', 'subeditor_submission_group_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'subeditor_submission_group_user_id');
$table->bigInteger('user_group_id');
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'subeditor_submission_group_user_group_id');
$table->index(['assoc_id', 'assoc_type'], 'subeditor_submission_group_assoc_id');
$table->unique(['context_id', 'assoc_id', 'assoc_type', 'user_id', 'user_group_id'], 'section_editors_unique');
});
// queries posted on submission workflow
Schema::create('queries', function (Blueprint $table) {
$table->comment('Discussions, usually related to a submission, created by editors, authors and other editorial staff.');
$table->bigInteger('query_id')->autoIncrement();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->smallInteger('stage_id');
$table->float('seq', 8, 2)->default(0);
$table->datetime('date_posted')->nullable();
$table->datetime('date_modified')->nullable();
$table->smallInteger('closed')->default(0);
$table->index(['assoc_type', 'assoc_id'], 'queries_assoc_id');
});
// queries posted on submission workflow
Schema::create('query_participants', function (Blueprint $table) {
$table->comment('The users assigned to a discussion.');
$table->bigIncrements('query_participant_id');
$table->bigInteger('query_id');
$table->foreign('query_id')->references('query_id')->on('queries')->onDelete('cascade');
$table->index(['query_id'], 'query_participants_query_id');
$table->bigInteger('user_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'query_participants_user_id');
$table->unique(['query_id', 'user_id'], 'query_participants_unique');
});
// List of all keywords.
Schema::create('submission_search_keyword_list', function (Blueprint $table) {
$table->comment('A list of all keywords used in the search index');
$table->bigInteger('keyword_id')->autoIncrement();
$table->string('keyword_text', 60);
$table->unique(['keyword_text'], 'submission_search_keyword_text');
});
// Indexed objects.
Schema::create('submission_search_objects', function (Blueprint $table) {
$table->comment('A list of all search objects indexed in the search index');
$table->bigInteger('object_id')->autoIncrement();
$table->bigInteger('submission_id');
$table->foreign('submission_id', 'submission_search_object_submission')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_id'], 'submission_search_objects_submission_id');
$table->integer('type')->comment('Type of item. E.g., abstract, fulltext, etc.');
$table->bigInteger('assoc_id')->comment('Optional ID of an associated record (e.g., a file_id)')->nullable();
});
// Keyword occurrences for each indexed object.
Schema::create('submission_search_object_keywords', function (Blueprint $table) {
$table->comment('Relationships between search objects and keywords in the search index');
$table->bigIncrements('submission_search_object_keyword_id');
$table->bigInteger('object_id');
$table->foreign('object_id')->references('object_id')->on('submission_search_objects')->onDelete('cascade');
$table->index(['object_id'], 'submission_search_object_keywords_object_id');
$table->bigInteger('keyword_id');
$table->foreign('keyword_id', 'submission_search_object_keywords_keyword_id')->references('keyword_id')->on('submission_search_keyword_list')->onDelete('cascade');
$table->index(['keyword_id'], 'submission_search_object_keywords_keyword_id');
$table->integer('pos')->comment('Word position of the keyword in the object.');
$table->unique(['object_id', 'pos'], 'submission_search_object_keywords_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('submission_search_object_keywords');
Schema::drop('submission_search_objects');
Schema::drop('submission_search_keyword_list');
Schema::drop('query_participants');
Schema::drop('queries');
Schema::drop('subeditor_submission_group');
Schema::drop('submission_comments');
Schema::drop('edit_decisions');
Schema::drop('author_settings');
Schema::drop('authors');
Schema::drop('publication_settings');
Schema::drop('submission_settings');
Schema::drop('submissions');
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file classes/migration/install/TemporaryFilesMigration.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 TemporaryFilesMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class TemporaryFilesMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Temporary file storage
Schema::create('temporary_files', function (Blueprint $table) {
$table->comment('Temporary files, e.g. where files are kept during an upload process before they are moved somewhere more appropriate.');
$table->bigInteger('file_id')->autoIncrement();
$table->bigInteger('user_id');
$table->foreign('user_id', 'temporary_files_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'temporary_files_user_id');
$table->string('file_name', 90);
$table->string('file_type', 255)->nullable();
$table->bigInteger('file_size');
$table->string('original_file_name', 127)->nullable();
$table->datetime('date_uploaded');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('temporary_files');
}
}
@@ -0,0 +1,78 @@
<?php
/**
* @file classes/migration/install/TombstoneMigration.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 TombstoneMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\install;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class TombstoneMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Unavailable data object tombstones.
Schema::create('data_object_tombstones', function (Blueprint $table) {
$table->comment('Entries for published data that has been removed. Usually used in the OAI endpoint.');
$table->bigInteger('tombstone_id')->autoIncrement();
$table->bigInteger('data_object_id');
$table->datetime('date_deleted');
$table->string('set_spec', 255);
$table->string('set_name', 255);
$table->string('oai_identifier', 255);
$table->index(['data_object_id'], 'data_object_tombstones_data_object_id');
});
// Data object tombstone settings.
Schema::create('data_object_tombstone_settings', function (Blueprint $table) {
$table->comment('More data about data object tombstones, including localized content.');
$table->bigIncrements('tombstone_setting_id');
$table->bigInteger('tombstone_id');
$table->foreign('tombstone_id', 'data_object_tombstone_settings_tombstone_id')->references('tombstone_id')->on('data_object_tombstones')->onDelete('cascade');
$table->index(['tombstone_id'], 'data_object_tombstone_settings_tombstone_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(['tombstone_id', 'locale', 'setting_name'], 'data_object_tombstone_settings_unique');
});
// Objects that are part of a data object tombstone OAI set.
Schema::create('data_object_tombstone_oai_set_objects', function (Blueprint $table) {
$table->comment('Relationships between tombstones and other data that can be collected in OAI sets, e.g. sections.');
$table->bigInteger('object_id')->autoIncrement();
$table->bigInteger('tombstone_id');
$table->foreign('tombstone_id', 'data_object_tombstone_oai_set_objects_tombstone_id')->references('tombstone_id')->on('data_object_tombstones')->onDelete('cascade');
$table->index(['tombstone_id'], 'data_object_tombstone_oai_set_objects_tombstone_id');
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('data_object_tombstone_oai_set_objects');
Schema::drop('data_object_tombstone_settings');
Schema::drop('data_object_tombstones');
}
}
@@ -0,0 +1,60 @@
<?php
/**
* @file classes/migration/upgrade/PKPv3_2_1SubeditorCategoriesMigration.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 PKPv3_2_1SubeditorCategoriesMigration
*
* @brief pkp/pkp-lib#5694 Allow subeditors to be assigned to Categories
*/
namespace PKP\migration\upgrade;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class PKPv3_2_1SubeditorCategoriesMigration extends \PKP\migration\Migration
{
private const ASSOC_TYPE_SECTION = 0x0000212;
/**
* Run the migrations.
*/
public function up(): void
{
// Schema changes
Schema::rename('section_editors', 'subeditor_submission_group');
Schema::table('subeditor_submission_group', function (Blueprint $table) {
// Change section_id to assoc_type/assoc_id
$table->bigInteger('assoc_type')->nullable();
$table->renameColumn('section_id', 'assoc_id');
// Drop indexes
$table->dropIndex('section_editors_pkey');
$table->dropIndex('section_editors_context_id');
$table->dropIndex('section_editors_section_id');
$table->dropIndex('section_editors_user_id');
// Create indexes
$table->index(['context_id'], 'section_editors_context_id');
$table->index(['assoc_id', 'assoc_type'], 'subeditor_submission_group_assoc_id');
$table->index(['user_id'], 'subeditor_submission_group_user_id');
$table->unique(['context_id', 'assoc_id', 'assoc_type', 'user_id'], 'section_editors_pkey');
});
// Populate the assoc_type data in the newly created column
DB::table('subeditor_submission_group')->update(['assoc_type' => self::ASSOC_TYPE_SECTION]);
}
/**
* Reverse the downgrades
*/
public function down(): void
{
throw new \PKP\install\DowngradeNotSupportedException();
}
}
@@ -0,0 +1,911 @@
<?php
/**
* @file classes/migration/upgrade/PKPv3_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 PKPv3_3_0UpgradeMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\upgrade;
use APP\core\Services;
use APP\facades\Repo;
use Exception;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\db\XMLDAO;
use PKP\file\FileManager;
abstract class PKPv3_3_0UpgradeMigration extends \PKP\migration\Migration
{
private const ASSOC_TYPE_REVIEW_ROUND = 0x000020B; //PKPApplication::ASSOC_TYPE_REVIEW_ROUND
private const ASSOC_TYPE_REVIEW_RESPONSE = 0x0000204; //PKPApplication::ASSOC_TYPE_REVIEW_RESPONSE
private const SUBMISSION_FILE_FAIR_COPY = 7; //SubmissionFile::SUBMISSION_FILE_FAIR_COPY
private const SUBMISSION_FILE_EDITOR = 8; //SubmissionFile::SUBMISSION_FILE_EDITOR
private const SUBMISSION_FILE_SUBMISSION = 2; //SubmissionFile::SUBMISSION_FILE_SUBMISSION
private const SUBMISSION_FILE_NOTE = 3; //SubmissionFile::SUBMISSION_FILE_NOTE
private const SUBMISSION_FILE_REVIEW_FILE = 4; //SubmissionFile::SUBMISSION_FILE_REVIEW_FILE
private const SUBMISSION_FILE_REVIEW_ATTACHMENT = 5; //SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT
private const SUBMISSION_FILE_REVIEW_REVISION = 15; //SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION
private const SUBMISSION_FILE_FINAL = 6; //SubmissionFile::SUBMISSION_FILE_FINAL
private const SUBMISSION_FILE_COPYEDIT = 9; //SubmissionFile::SUBMISSION_FILE_COPYEDIT
private const SUBMISSION_FILE_DEPENDENT = 17; //SubmissionFile::SUBMISSION_FILE_DEPENDENT
private const SUBMISSION_FILE_PROOF = 10; //SubmissionFile::SUBMISSION_FILE_PROOF
private const SUBMISSION_FILE_PRODUCTION_READY = 11; //SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY
private const SUBMISSION_FILE_ATTACHMENT = 13; //SubmissionFile::SUBMISSION_FILE_ATTACHMENT
private const SUBMISSION_FILE_QUERY = 18; //SubmissionFile::SUBMISSION_FILE_QUERY
abstract protected function getSubmissionPath(): string;
abstract protected function getContextPath(): string;
abstract protected function getContextTable(): string;
abstract protected function getContextKeyField(): string;
abstract protected function getContextSettingsTable(): string;
abstract protected function getSectionTable(): string;
abstract protected function getSerializedSettings(): array;
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasColumn('submissions', 'locale')) {
Schema::table('submissions', function (Blueprint $table) {
// pkp/pkp-lib#3572 Remove OJS 2.x upgrade tools (OPS doesn't have this)
$table->dropColumn('locale');
});
}
Schema::table('submissions', function (Blueprint $table) {
// pkp/pkp-lib#6285 submissions.section_id in OMP appears only from 3.2.1
if (Schema::hasColumn($table->getTable(), 'section_id')) {
// pkp/pkp-lib#2493 Remove obsolete columns
$table->dropColumn('section_id');
};
});
Schema::table('publication_settings', function (Blueprint $table) {
// pkp/pkp-lib#6096 DB field type TEXT is cutting off long content
$table->mediumText('setting_value')->nullable()->change();
});
if (Schema::hasColumn('authors', 'submission_id')) {
Schema::table('authors', function (Blueprint $table) {
// pkp/pkp-lib#2493 Remove obsolete columns
$table->dropColumn('submission_id');
});
}
Schema::table('announcements', function (Blueprint $table) {
// pkp/pkp-lib#5865 Change announcement expiry format in database
$table->date('date_expire')->change();
});
Schema::table('announcement_settings', function (Blueprint $table) {
// pkp/pkp-lib#6748 Change announcement setting type to permit nulls
$table->string('setting_type', 6)->nullable()->change();
});
// Transitional: The stage_id column may have already been added by the ADODB schema toolset
if (!Schema::hasColumn('email_templates_default', 'stage_id')) {
Schema::table('email_templates_default', function (Blueprint $table) {
// pkp/pkp-lib#4796 stage ID as a filter parameter to email templates
$table->bigInteger('stage_id')->nullable();
});
}
// pkp/pkp-lib#6301: Indexes may be missing that affect search performance.
// (These are added for 3.2.1-2 so may or may not be present for this upgrade code.)
$schemaManager = DB::getDoctrineSchemaManager();
if (!in_array('submissions_publication_id', array_keys($schemaManager->listTableIndexes('submissions')))) {
Schema::table('submissions', function (Blueprint $table) {
$table->index(['submission_id'], 'submissions_publication_id');
});
}
if (!in_array('submission_search_object_submission', array_keys($schemaManager->listTableIndexes('submission_search_objects')))) {
Schema::table('submission_search_objects', function (Blueprint $table) {
$table->index(['submission_id'], 'submission_search_object_submission');
});
}
// pkp/pkp-lib#6093 Don't allow nulls (previously an upgrade workaround)
Schema::table('announcement_types', function (Blueprint $table) {
$table->bigInteger('assoc_type')->nullable(false)->change();
});
Schema::table('email_templates', function (Blueprint $table) {
$table->bigInteger('context_id')->default(0)->nullable(false)->change();
});
Schema::table('genres', function (Blueprint $table) {
$table->bigInteger('seq')->nullable(false)->change();
$table->smallInteger('supplementary')->default(0)->nullable(false)->change();
});
Schema::table('event_log', function (Blueprint $table) {
$table->bigInteger('assoc_type')->nullable(false)->change();
$table->bigInteger('assoc_id')->nullable(false)->change();
});
Schema::table('email_log', function (Blueprint $table) {
$table->bigInteger('assoc_type')->nullable(false)->change();
$table->bigInteger('assoc_id')->nullable(false)->change();
});
Schema::table('notes', function (Blueprint $table) {
$table->bigInteger('assoc_type')->nullable(false)->change();
$table->bigInteger('assoc_id')->nullable(false)->change();
});
Schema::table('review_assignments', function (Blueprint $table) {
$table->bigInteger('review_round_id')->nullable(false)->change();
});
Schema::table('authors', function (Blueprint $table) {
$table->bigInteger('publication_id')->nullable(false)->change();
});
DB::unprepared('UPDATE review_assignments SET review_form_id=NULL WHERE review_form_id=0');
$this->_populateEmailTemplates();
$this->_makeRemoteUrlLocalizable();
// pkp/pkp-lib#6057: Migrate locale property from publications to submissions
Schema::table('submissions', function (Blueprint $table) {
$table->string('locale', 14)->nullable();
});
$updateQuery = DB::table('submissions as s')
->join('publications as p', 'p.publication_id', '=', 's.current_publication_id');
if (DB::connection() instanceof PostgresConnection) {
$updateQuery->updateFrom(['s.locale' => DB::raw('p.locale')]);
} else {
$updateQuery->update(['s.locale' => DB::raw('p.locale')]);
}
Schema::table('publications', function (Blueprint $table) {
$table->dropColumn('locale');
});
// pkp/pkp-lib#6057 Submission files refactor
$this->migrateSubmissionFiles();
$this->_fixCapitalCustomBlockTitles();
$this->_createCustomBlockTitles();
// Remove item views related to submission files and notes,
// and convert the assoc_id to an integer
DB::table('item_views')
->where('assoc_type', '!=', self::ASSOC_TYPE_REVIEW_RESPONSE)
->delete();
// PostgreSQL requires an explicit type cast
if (DB::connection() instanceof PostgresConnection) {
DB::statement('ALTER TABLE item_views ALTER COLUMN assoc_id TYPE BIGINT USING (assoc_id::INTEGER)');
} else {
Schema::table('item_views', function (Blueprint $table) {
$table->bigInteger('assoc_id')->change();
});
}
// pkp/pkp-lib#4017 and pkp/pkp-lib#4622
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue');
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
$table->index(['queue', 'reserved_at']);
});
// pkp/pkp-lib#6594 Clear context defaults installed for unused languages
$settingsWithDefaults = [
'authorInformation',
'librarianInformation',
'openAccessPolicy',
'privacyStatement',
'readerInformation',
'submissionChecklist',
'clockssLicense',
'lockssLicense',
];
$rows = DB::table($this->getContextSettingsTable() . ' as js')
->join($this->getContextSettingsTable() . ' as j', 'j.' . $this->getContextKeyField(), '=', 'js.' . $this->getContextKeyField())
->where('js.setting_name', '=', 'supportedFormLocales')
->select(['js.' . $this->getContextKeyField() . ' as id', 'js.setting_value as locales'])
->get();
foreach ($rows as $row) {
// account for some locale settings stored as assoc arrays
$locales = $row->locales;
if (empty($locales)) {
$locales = [];
} elseif (@unserialize($locales) !== false) {
$locales = unserialize($locales);
} else {
$locales = json_decode($locales, true);
}
$locales = array_values($locales);
DB::table($this->getContextSettingsTable())
->where($this->getContextKeyField(), '=', $row->id)
->whereIn('setting_name', $settingsWithDefaults)
->whereNotIn('locale', $locales)
->delete();
}
Schema::table($this->getContextSettingsTable(), function (Blueprint $table) {
// pkp/pkp-lib#6096 DB field type TEXT is cutting off long content
$table->mediumText('setting_value')->nullable()->change();
});
if (!Schema::hasColumn($this->getSectionTable(), 'is_inactive')) {
Schema::table($this->getSectionTable(), function (Blueprint $table) {
$table->smallInteger('is_inactive')->default(0);
});
}
if (Schema::hasTable('review_forms')) {
Schema::table('review_forms', function (Blueprint $table) {
$table->bigInteger('assoc_type')->nullable(false)->change();
$table->bigInteger('assoc_id')->nullable(false)->change();
});
}
// pkp/pkp-lib#6807 Make sure all submission last modification dates are set
DB::statement('UPDATE submissions SET last_modified = NOW() WHERE last_modified IS NULL');
$this->_settingsAsJSON();
if (Schema::hasColumn('author_settings', 'setting_type')) {
Schema::table('author_settings', function (Blueprint $table) {
// pkp/pkp-lib#2493 Remove obsolete columns
$table->dropColumn('setting_type');
});
}
}
/**
* Reverse the downgrades
*/
public function down(): void
{
throw new \PKP\install\DowngradeNotSupportedException();
}
/**
* @brief populate email templates with new records for workflow stage id
*/
private function _populateEmailTemplates()
{
$xmlDao = new XMLDAO();
$data = $xmlDao->parseStruct(Repo::emailTemplate()->dao->getMainEmailTemplatesFilename(), ['email']);
foreach ($data['email'] as $template) {
$attr = $template['attributes'];
if (array_key_exists('stage_id', $attr)) {
DB::table('email_templates_default')->where('email_key', $attr['key'])->update(['stage_id' => $attr['stage_id']]);
}
}
}
/**
* @brief make remoteUrl navigation item type multilingual and drop the url column
*/
private function _makeRemoteUrlLocalizable()
{
$contexts = DB::table($this->getContextTable() . ' AS c')
->join($this->getContextSettingsTable() . ' AS s', 's.' . $this->getContextKeyField(), '=', 'c.' . $this->getContextKeyField())
->where('s.setting_name', '=', 'supportedLocales')
->select(['c.' . $this->getContextKeyField() . ' AS id', 's.setting_value AS supported_locales'])
->get();
foreach ($contexts as $context) {
if (($locales = @unserialize($context->supported_locales)) === false) {
continue;
}
$navigationItems = DB::table('navigation_menu_items')->where('context_id', $context->id)->pluck('url', 'navigation_menu_item_id')->filter()->all();
foreach ($navigationItems as $navigation_menu_item_id => $url) {
foreach ($locales as $locale) {
DB::table('navigation_menu_item_settings')->insert([
'navigation_menu_item_id' => $navigation_menu_item_id,
'locale' => $locale,
'setting_name' => 'remoteUrl',
'setting_value' => $url,
'setting_type' => 'string'
]);
}
}
}
$site = DB::table('site')
->select('supported_locales')
->first();
$supportedLocales = @unserialize($site->supported_locales);
if ($supportedLocales !== false) {
$navigationItems = DB::table('navigation_menu_items')->where('context_id', '0')->pluck('url', 'navigation_menu_item_id')->filter()->all();
foreach ($navigationItems as $navigation_menu_item_id => $url) {
foreach ($supportedLocales as $locale) {
DB::table('navigation_menu_item_settings')->insert([
'navigation_menu_item_id' => $navigation_menu_item_id,
'locale' => $locale,
'setting_name' => 'remoteUrl',
'setting_value' => $url,
'setting_type' => 'string'
]);
}
}
}
Schema::table('navigation_menu_items', function (Blueprint $table) {
$table->dropColumn('url');
});
}
/**
* Migrate submission files after major refactor
*
* - Add files table to manage underlying file storage
* - Replace the use of file_id/revision as a unique id with a single
* auto-incrementing submission_file_id, and update all references.
* - Move revisions to a submission_file_revisons table.
* - Drop unused columns in submission_files table.
*
* @see pkp/pkp-lib#6057
*/
protected function migrateSubmissionFiles()
{
// pkp/pkp-lib#6616 Delete submission_files entries that correspond to nonexistent submissions
$orphanedIds = DB::table('submission_files AS sf')->leftJoin('submissions AS s', 'sf.submission_id', '=', 's.submission_id')->whereNull('s.submission_id')->pluck('sf.submission_id', 'sf.file_id');
foreach ($orphanedIds as $fileId => $submissionId) {
error_log("Removing orphaned submission_files entry ID {$fileId} with submission_id {$submissionId}");
DB::table('submission_files')->where('file_id', '=', $fileId)->delete();
}
// Add partial index (DBMS-specific)
if (DB::connection() instanceof PostgresConnection) {
DB::unprepared("CREATE INDEX event_log_settings_name_value ON event_log_settings (setting_name, setting_value) WHERE setting_name IN ('fileId', 'submissionId')");
} else {
DB::unprepared('CREATE INDEX event_log_settings_name_value ON event_log_settings (setting_name(50), setting_value(150))');
}
// Create a new table to track files in file storage
Schema::create('files', function (Blueprint $table) {
$table->bigIncrements('file_id');
$table->string('path', 255);
$table->string('mimetype', 255);
});
// Create a new table to track submission file revisions
Schema::create('submission_file_revisions', function (Blueprint $table) {
$table->bigIncrements('revision_id');
$table->unsignedBigInteger('submission_file_id');
$table->unsignedBigInteger('file_id');
});
// Add columns to submission_files table
Schema::table('submission_files', function (Blueprint $table) {
$table->unsignedBigInteger('new_file_id')->nullable(); // Renamed and made not nullable at the end of the migration
});
// Drop unique keys that will cause trouble while we're migrating
Schema::table('review_round_files', function (Blueprint $table) {
$table->dropIndex('review_round_files_pkey');
});
// Create entry in files and revisions tables for every submission_file
$fileManager = new FileManager();
$rows = DB::table('submission_files', 'sf')
->join('submissions AS s', 'sf.submission_id', '=', 's.submission_id')
->leftJoin('review_round_files AS rrf', function (JoinClause $j) {
$j->on('rrf.submission_id', '=', 'sf.submission_id')
->on('rrf.file_id', '=', 'sf.file_id')
->on('rrf.revision', '=', 'sf.revision');
})
->orderBy('sf.file_id')
->orderBy('rrf.review_round_id')
->orderBy('sf.revision')
->get([
's.context_id',
'sf.file_id',
'sf.revision',
'sf.submission_id',
'sf.genre_id',
'sf.file_type',
'sf.file_stage',
'sf.date_uploaded',
'sf.original_file_name',
'rrf.review_round_id'
]);
$fileService = Services::get('file');
$lastFileId = null;
$lastReviewRoundId = null;
$lastInsertedFileId = DB::table('submission_files')->max('file_id');
$fileIdMap = [];
foreach ($rows as $row) {
// Due to the new database structure, a file_id that spans across N+1 review rounds needs to be forked to a new file_id, to avoid being consolidated later on the upgrade process, which would cause data loss
// Statistics, logs, etc. will keep being referenced by the "main" file_id, which is not ideal, but also not possible to address.
// This branch will be accessed once the review_round_id gets changed within a group of the same file_ids
if ($lastFileId === $row->file_id && $lastReviewRoundId !== $row->review_round_id) {
$fileIdMap = [
$row->file_id => ++$lastInsertedFileId,
'sourceFileId' => $fileIdMap[$row->file_id] ?? $row->file_id
];
DB::table('submission_file_settings')->insertUsing(
['file_id', 'locale', 'setting_name', 'setting_value', 'setting_type'],
function (Builder $q) use ($row, $lastInsertedFileId) {
$q->from('submission_file_settings')
->where('file_id', '=', $row->file_id)
->select(DB::raw($lastInsertedFileId), 'locale', 'setting_name', 'setting_value', 'setting_type');
}
);
}
// Reproduces the removed method SubmissionFile::_generateFileName()
// genre is %s because it can be blank with review attachments
$filename = 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))
);
$path = sprintf(
'%s/%s/%s',
$this->getContextPath() . '/' . $row->context_id . '/' . $this->getSubmissionPath() . '/' . $row->submission_id,
$this->_fileStageToPath($row->file_stage),
$filename
);
if (!$fileService->fs->has($path)) {
error_log("A submission file was expected but not found at {$path}.");
}
$newFileId = DB::table('files')->insertGetId([
'path' => $path,
'mimetype' => $row->file_type,
], 'file_id');
$fileId = $fileIdMap[$row->file_id] ?? $row->file_id;
$updateData = ['new_file_id' => $newFileId];
// If the file_id was forked into a new one, we've got to update the references at the review_round_files and the submission_file itself
if ($fileId !== $row->file_id) {
$updateData['file_id'] = $fileId;
$updateData['source_file_id'] = $fileIdMap['sourceFileId'];
DB::table('review_round_files')
->where('file_id', '=', $row->file_id)
->where('revision', '=', $row->revision)
->update(['file_id' => $fileId]);
}
DB::table('submission_files')
->where('file_id', $row->file_id)
->where('revision', $row->revision)
->update($updateData);
DB::table('submission_file_revisions')->insert([
'submission_file_id' => $fileId,
'file_id' => $newFileId,
]);
// Update revision data in event logs
DB::table('event_log_settings as els')
->join(
'event_log_settings as file_setting',
fn (JoinClause $join) =>
$join->on('file_setting.log_id', '=', 'els.log_id')
->where('file_setting.setting_name', '=', 'fileId')
->where('file_setting.setting_value', '=', (string) $row->file_id)
)
->where('els.setting_name', 'fileRevision')
->where('els.setting_value', '=', $row->revision)
->update(['els.setting_value' => $newFileId]);
$lastFileId = $row->file_id;
$lastReviewRoundId = $row->review_round_id;
}
// Collect rows that will be deleted because they are old revisions
// They are identified by the new_file_id column, which is the only unique
// column on the table at this point.
$newFileIdsToDelete = [];
// Get all the unique file_ids. For each one, determine the latest revision
// in order to keep it in the table. The others will be flagged for removal
foreach (DB::table('submission_files')->select('file_id')->distinct()->get() as $row) {
$submissionFileRows = DB::table('submission_files')
->where('file_id', '=', $row->file_id)
->orderBy('revision', 'desc')
->get([
'file_id',
'new_file_id',
]);
$latestFileId = $submissionFileRows[0]->new_file_id;
foreach ($submissionFileRows as $submissionFileRow) {
if ($submissionFileRow->new_file_id !== $latestFileId) {
$newFileIdsToDelete[] = $submissionFileRow->new_file_id;
}
}
}
// Delete the rows for old revisions (chunked for performance)
foreach (array_chunk($newFileIdsToDelete, 100) as $chunkFileIds) {
DB::table('submission_files')
->whereIn('new_file_id', $chunkFileIds)
->delete();
}
// Remove all review round files that point to file ids
// that don't exist, so that the foreign key can be set
// up successfully.
// See: https://github.com/pkp/pkp-lib/issues/6337
DB::table('review_round_files as rrf')
->leftJoin('submission_files as sf', 'sf.file_id', '=', 'rrf.file_id')
->whereNotNull('rrf.file_id')
->whereNull('sf.file_id')
->delete();
// Update review round files
$rows = DB::table('review_round_files')->get();
foreach ($rows as $row) {
// Delete this row if another revision exists for this
// submission file. This ensures that when the revision
// column is dropped the submission_file_id column will
// be unique.
$count = DB::table('review_round_files')
->where('file_id', '=', $row->file_id)
->count();
if ($count > 1) {
DB::table('review_round_files')
->where('file_id', '=', $row->file_id)
->where('revision', '=', $row->revision)
->delete();
continue;
}
// Set assoc_type and assoc_id for all review round files
// Run this before migration to internal review file stages
DB::table('submission_files')
->where('file_id', '=', $row->file_id)
->whereIn('file_stage', [self::SUBMISSION_FILE_REVIEW_FILE, self::SUBMISSION_FILE_REVIEW_REVISION])
->update([
'assoc_type' => self::ASSOC_TYPE_REVIEW_ROUND,
'assoc_id' => $row->review_round_id,
]);
}
// Update name of event log params to reflect new file structure
DB::table('event_log_settings')
->where('setting_name', 'fileId')
->update(['setting_name' => 'submissionFileId']);
DB::table('event_log_settings')
->where('setting_name', 'fileRevision')
->update(['setting_name' => 'fileId']);
// Update file name of dependent files, see: pkp/pkp-lib#6801
DB::table('submission_files')
->select('file_id', 'original_file_name')
->where('file_stage', '=', self::SUBMISSION_FILE_DEPENDENT)
->chunkById(1000, function ($dependentFiles) {
foreach ($dependentFiles as $dependentFile) {
DB::table('submission_file_settings')
->where('file_id', '=', $dependentFile->file_id)
->where('setting_name', '=', 'name')
->update(['setting_value' => $dependentFile->original_file_name]);
}
}, 'file_id');
// Restructure submission_files and submission_file_settings tables
Schema::table('submission_files', function (Blueprint $table) {
$table->bigInteger('file_id')->nullable(false)->unsigned()->change();
});
Schema::table('submission_files', function (Blueprint $table) {
$table->dropPrimary(); // Drop compound primary key constraint
});
Schema::table('submission_files', function (Blueprint $table) {
$table->renameColumn('file_id', 'submission_file_id');
$table->renameColumn('new_file_id', 'file_id');
$table->renameColumn('source_file_id', 'source_submission_file_id');
$table->renameColumn('date_uploaded', 'created_at');
$table->renameColumn('date_modified', 'updated_at');
$table->dropColumn('revision');
$table->dropColumn('source_revision');
$table->dropColumn('file_size');
$table->dropColumn('file_type');
$table->dropColumn('original_file_name');
$table->primary('submission_file_id');
});
// pkp/pkp-lib#5804
$schemaManager = DB::getDoctrineSchemaManager();
if (!in_array('submission_files_stage_assoc', array_keys($schemaManager->listTableIndexes('submission_files')))) {
Schema::table('submission_files', function (Blueprint $table) {
$table->index(['file_stage', 'assoc_type', 'assoc_id'], 'submission_files_stage_assoc');
});
}
// Modify column types and attributes in separate migration
// function to prevent error in postgres with unfound columns
Schema::table('submission_files', function (Blueprint $table) {
$table->bigInteger('submission_file_id')->autoIncrement()->unsigned()->change();
$table->bigInteger('file_id')->nullable(false)->unsigned()->change();
$table->foreign('file_id')->references('file_id')->on('files');
});
Schema::table('submission_file_settings', function (Blueprint $table) {
$table->renameColumn('file_id', 'submission_file_id');
$table->string('setting_type', 6)->default('string')->change();
});
// Update columns in related tables
Schema::table('review_round_files', function (Blueprint $table) {
$table->renameColumn('file_id', 'submission_file_id');
$table->dropColumn('revision');
});
Schema::table('review_round_files', function (Blueprint $table) {
$table->bigInteger('submission_file_id')->nullable(false)->unique()->unsigned()->change();
$table->unique(['submission_id', 'review_round_id', 'submission_file_id'], 'review_round_files_pkey');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
});
Schema::table('review_files', function (Blueprint $table) {
$table->renameColumn('file_id', 'submission_file_id');
});
// pkp/pkp-lib#6616 Delete review_files entries that correspond to nonexistent submission_files
$orphanedIds = DB::table('review_files AS rf')
->select('rf.submission_file_id', 'rf.review_id')
->leftJoin('submission_files AS sf', 'rf.submission_file_id', '=', 'sf.submission_file_id')
->whereNull('sf.submission_file_id')
->whereNotNull('rf.submission_file_id')
->get();
foreach ($orphanedIds as $orphanedId) {
$reviewId = $orphanedId->{'review_id'};
$submissionFileId = $orphanedId->{'submission_file_id'};
error_log("Removing orphaned review_files entry with review_id ID {$reviewId} and submission_file_id {$submissionFileId}");
DB::table('review_files')
->where('submission_file_id', '=', $submissionFileId)
->where('review_id', '=', $reviewId)
->delete();
}
Schema::table('review_files', function (Blueprint $table) {
$table->bigInteger('submission_file_id')->nullable(false)->unsigned()->change();
$table->dropIndex('review_files_pkey');
$table->unique(['review_id', 'submission_file_id'], 'review_files_pkey');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
});
Schema::table('submission_file_revisions', function (Blueprint $table) {
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
$table->foreign('file_id')->references('file_id')->on('files');
});
// Postgres leaves the old file_id autoincrement sequence around, so
// we delete it and apply a new sequence.
if (DB::connection() instanceof PostgresConnection) {
DB::statement('DROP SEQUENCE submission_files_file_id_seq CASCADE');
Schema::table('submission_files', function (Blueprint $table) {
$table->bigIncrements('submission_file_id')->change();
});
}
}
/**
* Get the directory of a file based on its file stage
*
* @param int $fileStage One of SubmissionFile::SUBMISSION_FILE_ constants
*
* @return string
*/
private function _fileStageToPath($fileStage)
{
static $fileStagePathMap = [
self::SUBMISSION_FILE_SUBMISSION => 'submission',
self::SUBMISSION_FILE_NOTE => 'note',
self::SUBMISSION_FILE_REVIEW_FILE => 'submission/review',
self::SUBMISSION_FILE_REVIEW_ATTACHMENT => 'submission/review/attachment',
self::SUBMISSION_FILE_REVIEW_REVISION => 'submission/review/revision',
self::SUBMISSION_FILE_FINAL => 'submission/final',
self::SUBMISSION_FILE_FAIR_COPY => 'submission/fairCopy',
self::SUBMISSION_FILE_EDITOR => 'submission/editor',
self::SUBMISSION_FILE_COPYEDIT => 'submission/copyedit',
self::SUBMISSION_FILE_DEPENDENT => 'submission/proof',
self::SUBMISSION_FILE_PROOF => 'submission/proof',
self::SUBMISSION_FILE_PRODUCTION_READY => 'submission/productionReady',
self::SUBMISSION_FILE_ATTACHMENT => 'attachment',
self::SUBMISSION_FILE_QUERY => 'submission/query'
];
if (!isset($fileStagePathMap[$fileStage])) {
error_log('A file assigned to the file stage ' . $fileStage . ' could not be migrated.');
}
return $fileStagePathMap[$fileStage] ?? null;
}
/**
* Update block names to be all lowercase
*
* In previous versions, a custom block name would be stored in the
* array of blocks with capitals but the block name in the plugin_settings
* table is all lowercase. This migration aligns the two places by changing
* the block names to always use lowercase.
*
*/
private function _fixCapitalCustomBlockTitles()
{
$rows = DB::table('plugin_settings')
->where('plugin_name', 'customblockmanagerplugin')
->where('setting_name', 'blocks')
->get();
foreach ($rows as $row) {
$updateBlocks = false;
$blocks = unserialize($row->setting_value);
foreach ($blocks as $key => $block) {
$newBlock = strtolower_codesafe($block);
if ($block !== $newBlock) {
$blocks[$key] = $newBlock;
$updateBlocks = true;
}
}
if ($updateBlocks) {
DB::table('plugin_settings')
->where('plugin_name', 'customblockmanagerplugin')
->where('setting_name', 'blocks')
->where('context_id', $row->context_id)
->update(['setting_value' => serialize($blocks)]);
}
}
}
/**
* Create titles for custom block plugins
*
* This method copies the block names, which are a unique id,
* into a block setting, `blockTitle`, in the context's
* primary locale
*
* @see https://github.com/pkp/pkp-lib/issues/5619
*/
private function _createCustomBlockTitles()
{
$rows = DB::table('plugin_settings')
->where('plugin_name', 'customblockmanagerplugin')
->where('setting_name', 'blocks')
->get();
$newRows = [];
foreach ($rows as $row) {
$locale = DB::table($this->getContextTable())
->where($this->getContextKeyField(), $row->context_id)
->first()
->primary_locale;
$blocks = unserialize($row->setting_value);
foreach ($blocks as $block) {
$newRows[] = [
'plugin_name' => $block,
'context_id' => $row->context_id,
'setting_name' => 'blockTitle',
'setting_value' => serialize([$locale => $block]),
'setting_type' => 'object',
];
}
}
if (!DB::table('plugin_settings')->insert($newRows)) {
error_log('Failed to create title for custom blocks. This can be fixed manually by editing each custom block and adding a title.');
}
}
/**
* @brief reset serialized arrays and convert array and objects to JSON for serialization, see pkp/pkp-lib#5772
*/
private function _settingsAsJSON()
{
$serializedSettings = $this->getSerializedSettings();
$processedTables = [];
foreach ($serializedSettings as $tableName => $settings) {
$processedTables[] = $tableName;
foreach ($settings as $setting) {
DB::table($tableName)->where('setting_name', $setting)->get()->each(function ($row) use ($tableName) {
$this->_toJSON($row, $tableName, ['setting_name', 'locale'], 'setting_value');
});
}
}
// Convert settings where only setting_type column is available
$tables = DB::getDoctrineSchemaManager()->listTableNames();
foreach ($tables as $tableName) {
if (substr($tableName, -9) !== '_settings' || in_array($tableName, $processedTables)) {
continue;
}
if ($tableName === 'plugin_settings') {
DB::table($tableName)->where('setting_type', 'object')->get()->each(function ($row) use ($tableName) {
$this->_toJSON($row, $tableName, ['plugin_name', 'context_id', 'setting_name'], 'setting_value');
});
} elseif (Schema::hasColumn($tableName, 'setting_type')) {
try {
$settings = DB::table($tableName, 's')->where('setting_type', 'object')->get(['setting_name', 'setting_value', 's.*']);
} catch (Exception $e) {
error_log("Failed to migrate the settings entity \"{$tableName}\"\n" . $e);
continue;
}
$settings->each(fn ($row) => $this->_toJSON($row, $tableName, ['setting_name', 'locale'], 'setting_value'));
}
}
// Finally, convert values of other tables dependent from DAO::convertToDB
if (Schema::hasTable('review_form_responses')) {
DB::table('review_form_responses')->where('response_type', 'object')->get()->each(function ($row) {
$this->_toJSON($row, 'review_form_responses', ['review_id'], 'response_value');
});
}
DB::table('site')->get()->each(function ($row) {
$localeToConvert = function ($localeType) use ($row) {
$serializedValue = $row->{$localeType};
$oldLocaleValue = @unserialize($serializedValue);
if ($oldLocaleValue === false) {
return;
}
if (is_array($oldLocaleValue) && $this->_isNumerical($oldLocaleValue)) {
$oldLocaleValue = array_values($oldLocaleValue);
}
$newLocaleValue = json_encode($oldLocaleValue, JSON_UNESCAPED_UNICODE);
DB::table('site')->take(1)->update([$localeType => $newLocaleValue]);
};
$localeToConvert('installed_locales');
$localeToConvert('supported_locales');
});
}
/**
* @param object $row row representation
* @param string $tableName name of a settings table
* @param array $searchBy additional parameters to the where clause that should be combined with AND operator
* @param string $valueToConvert column name for values to convert to JSON
*/
private function _toJSON($row, $tableName, $searchBy, $valueToConvert)
{
// Check if value can be unserialized
$serializedOldValue = $row->{$valueToConvert};
$oldValue = @unserialize($serializedOldValue);
if ($oldValue === false) {
return;
}
// Reset arrays to avoid keys being mixed up
if (is_array($oldValue) && $this->_isNumerical($oldValue)) {
$oldValue = array_values($oldValue);
}
$newValue = json_encode($oldValue, JSON_UNESCAPED_UNICODE); // don't convert utf-8 characters to unicode escaped code
$id = array_key_first((array)$row); // get first/primary key column
// Remove empty filters
$searchBy = array_filter($searchBy, function ($item) use ($row) {
if (empty($row->{$item})) {
return false;
}
return true;
});
$queryBuilder = DB::table($tableName)->where($id, $row->{$id});
foreach ($searchBy as $key => $column) {
$queryBuilder = $queryBuilder->where($column, $row->{$column});
}
$queryBuilder->update([$valueToConvert => $newValue]);
}
/**
* @param array $array to check
*
* @return bool
*
* @brief checks unserialized array; returns true if array keys are integers
* otherwise if keys are mixed and sequence starts from any positive integer it will be serialized as JSON object instead of an array
* See pkp/pkp-lib#5690 for more details
*/
private function _isNumerical($array)
{
foreach ($array as $item => $value) {
if (!is_integer($item)) {
return false;
} // is an associative array;
}
return true;
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/FailedJobsMigration.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 FailedJobsMigration
*
* @brief Describe database table structures.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FailedJobsMigration extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
// Schema matches https://github.com/illuminate/queue/blob/7.x/Console/stubs/failed_jobs.stub
Schema::create('failed_jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migration.
*/
public function down()
{
Schema::dropIfExists('failed_jobs');
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I2890_AddSetNullForOnDeleteToReviewRoundIdForeign.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 I2890_AddSetNullForOnDeleteToReviewRoundIdForeign
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I2890_AddSetNullForOnDeleteToReviewRoundIdForeign extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('edit_decisions', function (Blueprint $table) {
$table->dropForeign(['review_round_id']);
$table
->foreign('review_round_id')
->references('review_round_id')
->on('review_rounds')
->onDelete('set null');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('edit_decisions', function (Blueprint $table) {
$table->dropForeign(['review_round_id']);
$table
->foreign('review_round_id')
->references('review_round_id')
->on('review_rounds');
});
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I2890_EmailTemplatesVarcharLengthUpdate.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 I2890_EmailTemplatesVarcharLengthUpdate
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I2890_EmailTemplatesVarcharLengthUpdate extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('email_templates', function (Blueprint $table) {
$table->string('email_key', 255)->change();
});
Schema::table('email_templates_default', function (Blueprint $table) {
$table->string('email_key', 255)->change();
});
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->string('email_key', 255)->change();
$table->string('subject', 255)->change();
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('email_templates', function (Blueprint $table) {
$table->string('email_key', 64)->change();
});
Schema::table('email_templates_default', function (Blueprint $table) {
$table->string('email_key', 64)->change();
});
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->string('email_key', 64)->change();
$table->string('subject', 120)->change();
});
}
}
@@ -0,0 +1,214 @@
<?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
*
* @brief Add primary keys to tables that do not currently have them, to support DB replication.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
abstract class I3573_AddPrimaryKeys extends \PKP\migration\Migration
{
/**
* Run the migrations
*/
public function up(): void
{
// Rename tablename_pkey unique indexes to tablename_unique to avoid PostgreSQL collision
foreach (static::getIndexData() as $tableName => [$oldIndexName, $columns, $newIndexName, $isUnique]) {
// If the table does not exist, do not process it.
if (!Schema::hasTable($tableName)) {
continue;
}
if (DB::getDoctrineSchemaManager()->introspectTable($tableName)->hasIndex($oldIndexName)) {
// Depending on whether the schema was created with ADODB or Laravel schema management, user_settings_pkey
// will either be a constraint or an index. See https://github.com/pkp/pkp-lib/issues/7670.
try {
Schema::table($tableName, fn (Blueprint $table) => $table->dropUnique($oldIndexName));
} catch (Exception $e) {
Schema::table($tableName, fn (Blueprint $table) => $table->dropIndex($oldIndexName));
}
}
if ($isUnique) {
Schema::table($tableName, fn (Blueprint $table) => $table->unique($columns, $newIndexName));
} else {
Schema::table($tableName, fn (Blueprint $table) => $table->index($columns, $newIndexName));
}
}
// Add the autoincrement columns
foreach (static::getKeyNames() as $tableName => $keyName) {
// If the table does not exist, do not process it.
if (!Schema::hasTable($tableName)) {
continue;
}
// If the table already has the named column, do not process it.
if (Schema::hasColumn($tableName, $keyName)) {
continue;
}
Schema::table($tableName, function (Blueprint $table) use ($keyName) {
$table->bigIncrements($keyName)->first();
});
}
}
/**
* Reverse the migrations
*/
public function down(): void
{
// Remove the autoincrement columns
foreach (static::getKeyNames() as $tableName => $keyName) {
// If the table does not exist, do not process it.
if (!Schema::hasTable($tableName)) {
continue;
}
// If the table does not have the named column, do not process it.
if (!Schema::hasColumn($tableName, $keyName)) {
continue;
}
// Handle special case: notification_subscription_settings already had setting_id prior to this migration. Do not drop it.
if ($tableName === 'notification_subscription_settings') {
continue;
}
Schema::table($tableName, function (Blueprint $table) use ($keyName) {
$table->unsignedInteger($keyName)->change(); // Drop auto increment
$table->dropPrimary($keyName);
$table->dropColumn($keyName);
});
}
// Rename tablename_unique indexes back to tablename_pkey
foreach (static::getIndexData() as $tableName => [$oldIndexName, $columns, $newIndexName, $isUnique]) {
// If the table does not exist, do not process it.
if (!Schema::hasTable($tableName)) {
continue;
}
Schema::table($tableName, function (Blueprint $table) use ($columns, $oldIndexName, $newIndexName, $isUnique) {
if ($isUnique) {
$table->dropUnique($newIndexName);
$table->unique($columns, $oldIndexName);
} else {
$table->dropIndex($newIndexName);
$table->index($columns, $oldIndexName);
}
});
}
}
public static function getKeyNames(): array
{
return [
'site' => 'site_id',
'versions' => 'version_id',
'oai_resumption_tokens' => 'oai_resumption_token_id',
'review_round_files' => 'review_round_file_id',
'user_interests' => 'user_interest_id',
'email_templates_default_data' => 'email_templates_default_data_id',
'subeditor_submission_group' => 'subeditor_submission_group_id',
'scheduled_tasks' => 'scheduled_task_id',
'announcement_settings' => 'announcement_setting_id',
'publication_categories' => 'publication_category_id',
'user_user_groups' => 'user_user_group_id',
'review_form_responses' => 'review_form_response_id',
'query_participants' => 'query_participant_id',
'user_group_stage' => 'user_group_stage_id',
'email_log_users' => 'email_log_user_id',
'review_files' => 'review_file_id',
'submission_search_object_keywords' => 'submission_search_object_keyword_id',
'announcement_type_settings' => 'announcement_type_setting_id',
'author_settings' => 'author_setting_id',
'category_settings' => 'category_setting_id',
'citation_settings' => 'citation_setting_id',
'controlled_vocab_entry_settings' => 'controlled_vocab_entry_setting_id',
'data_object_tombstone_settings' => 'tombstone_setting_id',
'doi_settings' => 'doi_setting_id', // Already created with schema during upgrade
'email_templates_settings' => 'email_template_setting_id',
'event_log_settings' => 'event_log_setting_id',
'filter_settings' => 'filter_setting_id',
'genre_settings' => 'genre_setting_id',
'institution_settings' => 'institution_setting_id', // Already created with schema during upgrade
'library_file_settings' => 'library_file_setting_id',
'navigation_menu_item_assignment_settings' => 'navigation_menu_item_assignment_setting_id',
'navigation_menu_item_settings' => 'navigation_menu_item_setting_id',
'notification_settings' => 'notification_setting_id',
'notification_subscription_settings' => 'setting_id', // Already had this ID before upgrade
'plugin_settings' => 'plugin_setting_id',
'publication_settings' => 'publication_setting_id',
'review_form_element_settings' => 'review_form_element_setting_id',
'review_form_settings' => 'review_form_setting_id',
'submission_file_settings' => 'submission_file_setting_id',
'submission_settings' => 'submission_setting_id',
'user_group_settings' => 'user_group_setting_id',
'user_settings' => 'user_setting_id',
'site_settings' => 'site_setting_id',
'static_page_settings' => 'static_page_setting_id', // PLUGIN
];
}
/**
* Get a list of index data for tables that need their indexes rebuilt/renamed.
*
* @return array [table_name => [old_key_name, [list, of, columns], new_key_name, is_unique]
*/
public static function getIndexData(): array
{
return [
'review_form_responses' => ['review_form_responses_pkey', ['review_form_element_id', 'review_id'], 'review_form_responses_unique', false],
'review_round_files' => ['review_round_files_pkey', ['submission_id', 'review_round_id', 'submission_file_id'], 'review_round_files_unique', true],
'review_files' => ['review_files_pkey', ['review_id', 'submission_file_id'], 'review_files_unique', true],
'scheduled_tasks' => ['scheduled_tasks_pkey', ['class_name'], 'scheduled_tasks_unique', true],
'announcement_type_settings' => ['announcement_type_settings_pkey', ['type_id', 'locale', 'setting_name'], 'announcement_type_settings_unique', true],
'announcement_settings' => ['announcement_settings_pkey', ['announcement_id', 'locale', 'setting_name'], 'announcement_settings_unique', true],
'category_settings' => ['category_settings_pkey', ['category_id', 'locale', 'setting_name'], 'category_settings_unique', true],
'versions' => ['versions_pkey', ['product_type', 'product', 'major', 'minor', 'revision', 'build'], 'versions_unique', true],
'site_settings' => ['site_settings_pkey', ['setting_name', 'locale'], 'site_settings_unique', true],
'user_settings' => ['user_settings_pkey', ['user_id', 'locale', 'setting_name'], 'user_settings_unique', true],
'notification_settings' => ['notification_settings_pkey', ['notification_id', 'locale', 'setting_name'], 'notification_settings_unique', true],
'email_templates_default_data' => ['email_templates_default_data_pkey', ['email_key', 'locale'], 'email_templates_default_data_unique', true],
'email_templates_settings' => ['email_settings_pkey', ['email_id', 'locale', 'setting_name'], 'email_templates_settings_unique', true],
'oai_resumption_tokens' => ['oai_resumption_tokens_pkey', ['token'], 'oai_resumption_tokens_unique', true],
'plugin_settings' => ['plugin_settings_pkey', ['plugin_name', 'context_id', 'setting_name'], 'plugin_settings_unique', true],
'genre_settings' => ['genre_settings_pkey', ['genre_id', 'locale', 'setting_name'], 'genre_settings_unique', true],
'library_file_settings' => ['library_file_settings_pkey', ['file_id', 'locale', 'setting_name'], 'library_file_settings_unique', true],
'event_log_settings' => ['event_log_settings_pkey', ['log_id', 'setting_name'], 'event_log_settings_unique', true],
'citation_settings' => ['citation_settings_pkey', ['citation_id', 'locale', 'setting_name'], 'citation_settings_unique', true],
'filter_settings' => ['filter_settings_pkey', ['filter_id', 'locale', 'setting_name'], 'filter_settings_unique', true],
'navigation_menu_item_settings' => ['navigation_menu_item_settings_pkey', ['navigation_menu_item_id', 'locale', 'setting_name'], 'navigation_menu_item_settings_unique', true],
'navigation_menu_item_assignment_settings' => ['navigation_menu_item_assignment_settings_pkey', ['navigation_menu_item_assignment_id', 'locale', 'setting_name'], 'navigation_menu_item_assignment_settings_unique', true],
'review_form_settings' => ['review_form_settings_pkey', ['review_form_id', 'locale', 'setting_name'], 'review_form_settings_unique', true],
'review_form_element_settings' => ['review_form_element_settings_pkey', ['review_form_element_id', 'locale', 'setting_name'], 'review_form_element_settings_unique', true],
'user_group_settings' => ['user_group_settings_pkey', ['user_group_id', 'locale', 'setting_name'], 'user_group_settings_unique', true],
'user_user_groups' => ['user_user_groups_pkey', ['user_group_id', 'user_id'], 'user_user_groups_unique', true],
'user_group_stage' => ['user_group_stage_pkey', ['context_id', 'user_group_id', 'stage_id'], 'user_group_stage_unique', true],
'submission_file_settings' => ['submission_file_settings_pkey', ['submission_file_id', 'locale', 'setting_name'], 'submission_file_settings_unique', true],
'submission_settings' => ['submission_settings_pkey', ['submission_id', 'locale', 'setting_name'], 'submission_settings_unique', true],
'publication_settings' => ['publication_settings_pkey', ['publication_id', 'locale', 'setting_name'], 'publication_settings_unique', true],
'author_settings' => ['author_settings_pkey', ['author_id', 'locale', 'setting_name'], 'author_settings_unique', true],
'subeditor_submission_group' => ['section_editors_pkey', ['context_id', 'assoc_id', 'assoc_type', 'user_id'], 'section_editors_unique', true],
'query_participants' => ['query_participants_pkey', ['query_id', 'user_id'], 'query_participants_unique', true],
'submission_search_object_keywords' => ['submission_search_object_keywords_pkey', ['object_id', 'pos'], 'submission_search_object_keywords_unique', true],
'data_object_tombstone_settings' => ['data_object_tombstone_settings_pkey', ['tombstone_id', 'locale', 'setting_name'], 'data_object_tombstone_settings_unique', true],
];
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I4789_AddReviewerRequestResentColumns.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 I4789_AddReviewerRequestResentColumns
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I4789_AddReviewerRequestResentColumns extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('review_assignments', function (Blueprint $table) {
$table->smallInteger('request_resent')->default(0);
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('review_assignments', function (Blueprint $table) {
$table->dropColumn('request_resent');
});
}
}
@@ -0,0 +1,512 @@
<?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 PKP\migration\upgrade\v3_4_0;
use APP\facades\Repo;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\db\XMLDAO;
use PKP\facades\Locale;
use PKP\migration\Migration;
use stdClass;
abstract class I5716_EmailTemplateAssignments extends Migration
{
/** @var array $newEmailIds New emails created during the migration that should be removed on downgrade */
public array $newEmailIds = [];
abstract protected function getContextTable(): string;
abstract protected function getContextSettingsTable(): string;
abstract protected function getContextIdColumn(): string;
public function up(): void
{
$contextIds = DB::table($this->getContextTable())
->pluck($this->getContextIdColumn());
$this->moveDisabledEmailTemplateSettings($contextIds);
$this->migrateNotificationCenterTemplates();
Schema::table('email_templates', function (Blueprint $table) {
$table->dropColumn('enabled');
$table->string('alternate_to', 255)->nullable();
$table->index(['alternate_to'], 'email_templates_alternate_to');
});
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->string('name', 255)->nullable();
});
$this->addDefaultTemplateNames();
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->string('name', 255)->change();
});
// The order below is important
$this->assignIncludedAlternateTemplates($contextIds);
$this->assignRemainingCustomTemplates($contextIds);
$this->createAlternateTemplateNames($contextIds);
}
public function down(): void
{
Schema::table('email_templates', function (Blueprint $table) {
$table->smallInteger('enabled')->default(1);
$table->dropIndex('email_templates_alternate_to');
$table->dropColumn('alternate_to');
});
DB::table('email_templates_settings')
->where('setting_name', 'name')
->delete();
$this->downgradeEditorAssignTemplate();
$this->downgradeAssignRemainingCustomTemplates();
}
/**
* Add context settings for disabled email templates
*
* This sets the `submissionAcknowledgement` and `editorialStatsEmail`
* context settings based on whether the related email templates have
* been disabled.
*
* From 3.4, emails will no longer be disabled at the email template
* level.
*/
public function moveDisabledEmailTemplateSettings(Collection $contextIds): void
{
$contextIds->each(function (int $contextId) {
if (
DB::table('email_templates')
->where('context_id', $contextId)
->where('enabled', 0)
->where('email_key', 'SUBMISSION_ACK')
->exists()
) {
$submissionAckSetting = '';
} elseif (
DB::table('email_templates')
->where('context_id', $contextId)
->where('enabled', 0)
->where('email_key', 'SUBMISSION_ACK_NOT_USER')
->exists()
) {
$submissionAckSetting = 'submittingAuthor';
} else {
$submissionAckSetting = 'allAuthors';
}
$statsReportSetting = !DB::table('email_templates')
->where('context_id', $contextId)
->where('enabled', 0)
->where('email_key', 'STATISTICS_REPORT_NOTIFICATION')
->exists();
DB::table($this->getContextSettingsTable())->insert([
[
$this->getContextIdColumn() => $contextId,
'setting_name' => 'submissionAcknowledgement',
'setting_value' => $submissionAckSetting,
],
[
$this->getContextIdColumn() => $contextId,
'setting_name' => 'editorialStatsEmail',
'setting_value' => $statsReportSetting,
],
]);
});
}
/**
* Migrates all custom NOTIFICATION_CENTER_DEFAULT templates
*
* This was the default template for the "Notify" feature
* from the participants list in the workflow.
*
* We create a copy of the default template for each discussion
* stage mailable. Then, if journals customized this template,
* we update the keys for that custom template and make copies
* of it for each stage mailable.
*/
protected function migrateNotificationCenterTemplates(): void
{
$alternateTos = $this->getDiscussionTemplates();
$newDefaultKeys = clone ($alternateTos);
$newDefaultKey = $newDefaultKeys->shift();
DB::table('email_templates_default_data')
->where('email_key', 'NOTIFICATION_CENTER_DEFAULT')
->update(['email_key' => $newDefaultKey]);
$newDefaultKeys->each(function (string $key) use ($newDefaultKey) {
DB::table('email_templates_default_data')
->where('email_key', $newDefaultKey)
->get()
->each(function (stdClass $row) use ($key) {
DB::table('email_templates_default_data')
->insert([
'email_key' => $key,
'locale' => $row->locale,
'subject' => $row->subject,
'body' => $row->body,
]);
});
});
DB::table('email_templates')
->where('email_key', 'NOTIFICATION_CENTER_DEFAULT')
->get()
->each(function (stdClass $row) use ($alternateTos) {
$keys = clone $alternateTos;
DB::table('email_templates')
->where('email_id', $row->email_id)
->update(['email_key' => $keys->shift()]);
$settingsRows = DB::table('email_templates_settings')
->where('email_id', $row->email_id)
->get();
$keys->each(function (string $key) use ($row, $settingsRows) {
DB::table('email_templates')
->insert([
'email_key' => $key,
'context_id' => $row->context_id,
]);
$newEmailId = DB::getPdo()->lastInsertId();
if ($settingsRows->count()) {
DB::table('email_templates_settings')->insert(
$settingsRows->map(fn (stdClass $settingsRow) => [
'email_id' => $newEmailId,
'locale' => $settingsRow->locale,
'setting_name' => $settingsRow->setting_name,
'setting_value' => $settingsRow->setting_value,
])->toArray()
);
}
});
});
}
/**
* Adds name values to the default email templates
*
* Localized names are defined in the emailTemplates.xml file.
*
* After setting the names, any default templates that don't have
* a name will be given a name from the email key.
*/
protected function addDefaultTemplateNames(): void
{
$xmlDao = new XMLDAO();
$data = $xmlDao->parseStruct('registry/emailTemplates.xml', ['email']);
if (!isset($data['email'])) {
throw new Exception('Failed to load or parse registry/emailTemplates.xml');
}
$locales = json_decode(
DB::table('site')
->pluck('installed_locales')
->first()
);
$initialMissingLocaleKeyHandler = Locale::getMissingKeyHandler();
Locale::setMissingKeyHandler(fn (string $key): string => '');
foreach ($locales as $locale) {
Locale::setLocale($locale);
foreach ($data['email'] as $entry) {
$key = $entry['attributes']['key'];
$name = $entry['attributes']['name'];
$name = __($name, [], $locale);
DB::table('email_templates_default_data')
->where('email_key', $key)
->where('locale', $locale)
->update(['name' => $name ? $name : $key]);
}
}
Locale::setMissingKeyHandler($initialMissingLocaleKeyHandler);
DB::table('email_templates_default_data')
->whereNull('name')
->get()
->each(function (stdClass $row) {
DB::table('email_templates_default_data')
->where('email_key', $row->email_key)
->where('locale', $row->locale)
->update(['name' => $row->email_key]);
});
}
/**
* Creates entries in email_templates for all of the alternate
* templates that are included by default in the application
*
* These are the old email templates that would be available
* in the "notify participant" feature.
*
* This migration will make them "alternate to" the appropriate
* discussion mailables.
*
* It sets the alternate_to column and creates copies for
* the discussion mailable in each stage.
*/
protected function assignIncludedAlternateTemplates(Collection $contextIds): void
{
$contextIds->each(function (int $contextId) {
foreach ($this->mapIncludedAlternateTemplates() as $key => $alternateTo) {
DB::table('email_templates')->updateOrInsert(
[
'email_key' => $key,
'context_id' => $contextId,
],
[
'alternate_to' => $alternateTo,
]
);
}
});
$this->modifyEditorAssignTemplate($contextIds);
}
/**
* Install the new EDITOR_ASSIGN_<stage> templates
*
* Install the default templates. If a custom template has been
* created for the EDITOR_ASSIGN template, copy this custom
* template to the new EDITOR_ASSIGN_<stage> templates.
*/
protected function modifyEditorAssignTemplate(Collection $contextIds): void
{
$newTemplates = $this->mapEditorAssignTemplates();
$newTemplates->each(
function (string $alternateTo, string $key) {
Repo::emailTemplate()->dao->installEmailTemplates(
Repo::emailTemplate()->dao->getMainEmailTemplatesFilename(),
[],
$key
);
}
);
$contextIds->each(function (int $contextId) use ($newTemplates) {
$customTemplateId = DB::table('email_templates')
->where('context_id', $contextId)
->where('email_key', 'EDITOR_ASSIGN')
->pluck('email_id')
->first();
if (!$customTemplateId) {
return;
}
$rows = DB::table('email_templates_settings')
->where('email_id', $customTemplateId)
->get();
DB::table('email_templates')
->where('context_id', $contextId)
->whereIn('email_key', $newTemplates->keys())
->pluck('email_id')
->each(function (int $emailId) use ($rows) {
DB::table('email_templates_settings')
->insert(
$rows->map(
function (stdClass $row) use ($emailId) {
return [
'email_id' => $emailId,
'locale' => $row->locale,
'setting_name' => $row->setting_name,
'setting_value' => $row->setting_value,
];
}
)->toArray()
);
});
});
}
/**
* Reset the editor assign template to its original key
*/
protected function downgradeEditorAssignTemplate(): void
{
$emailIds = DB::table('email_templates')
->where('email_key', 'EDITOR_ASSIGN_SUBMISSION')
->orWhere('email_key', 'EDITOR_ASSIGN_REVIEW')
->orWhere('email_key', 'EDITOR_ASSIGN_PRODUCTION')
->pluck('email_id');
DB::table('email_templates')
->whereIn('email_id', $emailIds->toArray())
->delete();
DB::table('email_templates_settings')
->whereIn('email_id', $emailIds->toArray())
->delete();
}
/**
* Assign all remaining unassigned custom templates to the
* discussion stage mailables.
*
* Assigns the existing custom template to the first discussion
* mailable that exists. Then creates copies of the template for
* each of the remaining discussions.
*/
protected function assignRemainingCustomTemplates(Collection $contextIds): void
{
$contextIds->each(function (int $contextId) {
DB::table('email_templates as et')
->leftJoin('email_templates_default_data as etdd', 'et.email_key', '=', 'etdd.email_key')
->where('et.context_id', $contextId)
->whereNull('et.alternate_to')
->whereNull('etdd.email_key')
->get(['et.email_id', 'et.email_key'])
->each(function (stdClass $row) use ($contextId) {
$alternateTos = $this->getDiscussionTemplates();
DB::table('email_templates')
->where('email_id', $row->email_id)
->update(['alternate_to' => $alternateTos->shift()]);
if (!$alternateTos->count()) {
return;
}
$settingsRows = DB::table('email_templates_settings')
->where('email_id', $row->email_id)
->get();
$alternateTos->each(function (string $alternateTo) use ($row, $settingsRows, $contextId) {
DB::table('email_templates')->insert([
'email_key' => $row->email_key . '_' . $alternateTo,
'context_id' => $contextId,
'alternate_to' => $alternateTo,
]);
$newEmailId = DB::getPdo()->lastInsertId();
$settingsRows->each(function (stdClass $settingsRow) use ($newEmailId) {
DB::table('email_templates_settings')->insert([
'email_id' => $newEmailId,
'locale' => $settingsRow->locale,
'setting_name' => $settingsRow->setting_name,
'setting_value' => $settingsRow->setting_value,
]);
});
$this->newEmailIds[] = $newEmailId;
});
});
});
}
/**
* Delete the extra copies of custom templates created
* in self::assignRemainingCustomTemplates()
*/
protected function downgradeAssignRemainingCustomTemplates(): void
{
DB::table('email_templates')
->whereIn('email_id', $this->newEmailIds)
->delete();
DB::table('email_templates_settings')
->whereIn('email_id', $this->newEmailIds)
->delete();
}
/**
* Create a name for all custom templates that are an alternate
* to another template.
*
* Generates the name from the email key. Example:
*
* MY_EXAMPLE_KEY becomes MY EXAMPLE KEY
*
*/
protected function createAlternateTemplateNames(Collection $contextIds): void
{
$primaryLocales = DB::table($this->getContextTable())
->get([
$this->getContextIdColumn() . ' as context_id',
'primary_locale',
]);
$contextIds->each(function (int $contextId) use ($primaryLocales) {
$primaryLocale = $primaryLocales
->first(fn ($row) => $row->context_id === $contextId)
->primary_locale;
$nameRows = DB::table('email_templates')
->where('context_id', $contextId)
->whereNotNull('alternate_to')
->get(['email_id', 'email_key'])
->map(function ($row) use ($primaryLocale) {
return [
'email_id' => $row->email_id,
'locale' => $primaryLocale,
'setting_name' => 'name',
'setting_value' => str_replace('_', ' ', $row->email_key),
];
});
DB::table('email_templates_settings')->insert($nameRows->toArray());
});
}
/**
* Get a map of the alternate templates to be reassigned
*
* @return [email_key => alternate_to]
*/
protected function mapIncludedAlternateTemplates(): array
{
return [
'COPYEDIT_REQUEST' => 'DISCUSSION_NOTIFICATION_COPYEDITING',
'LAYOUT_REQUEST' => 'DISCUSSION_NOTIFICATION_PRODUCTION',
'LAYOUT_COMPLETE' => 'DISCUSSION_NOTIFICATION_PRODUCTION',
];
}
/**
* Get a map of the EDITOR_ASSIGN_<stage> templates
*
* @return [email_key => alternate_to]
*/
protected function mapEditorAssignTemplates(): Collection
{
return collect([
'EDITOR_ASSIGN_SUBMISSION' => 'DISCUSSION_NOTIFICATION_SUBMISSION',
'EDITOR_ASSIGN_REVIEW' => 'DISCUSSION_NOTIFICATION_REVIEW',
'EDITOR_ASSIGN_PRODUCTION' => 'DISCUSSION_NOTIFICATION_PRODUCTION',
]);
}
/**
* Get all discussion mailable temlate keys in this app
*/
abstract protected function getDiscussionTemplates(): Collection;
}
@@ -0,0 +1,334 @@
<?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 PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
abstract class I6093_AddForeignKeys extends \PKP\migration\Migration
{
abstract protected function getContextTable(): string;
abstract protected function getContextSettingsTable(): string;
abstract protected function getContextKeyField(): string;
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('announcement_types', function (Blueprint $table) {
// Drop the old assoc_type column and assoc-based index
$table->dropIndex('announcement_types_assoc');
$table->dropColumn('assoc_type');
// Rename assoc_id to context_id and introduce foreign key constraint
$table->renameColumn('assoc_id', 'context_id');
$table->foreign('context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
// Introduce new index
$table->index(['context_id'], 'announcement_types_context_id');
});
Schema::table('announcement_type_settings', function (Blueprint $table) {
$table->foreign('type_id')->references('type_id')->on('announcement_types')->onDelete('cascade');
});
Schema::table('announcements', function (Blueprint $table) {
$table->foreign('type_id')->references('type_id')->on('announcement_types')->onDelete('set null');
$table->index(['type_id'], 'announcements_type_id');
});
Schema::table('announcement_settings', function (Blueprint $table) {
$table->foreign('announcement_id')->references('announcement_id')->on('announcements')->onDelete('cascade');
});
Schema::table('category_settings', function (Blueprint $table) {
$table->dropColumn('setting_type');
$table->foreign('category_id')->references('category_id')->on('categories')->onDelete('cascade');
});
// Permit nulls in categories.parent_id where previously 0 was used for "no parent"
Schema::table('categories', function (Blueprint $table) {
$table->bigInteger('parent_id')->nullable()->change();
});
DB::table('categories')->where('parent_id', '=', 0)->update(['parent_id' => null]);
Schema::table('categories', function (Blueprint $table) {
$table->dropIndex('category_context_id');
$table->index(['context_id', 'parent_id'], 'category_context_parent_id');
$table->foreign('context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->index(['context_id'], 'category_context_id');
$table->foreign('parent_id')->references('category_id')->on('categories')->onDelete('set null');
$table->index(['parent_id'], 'category_parent_id');
});
Schema::table('publication_categories', function (Blueprint $table) {
$table->foreign('category_id')->references('category_id')->on('categories')->onDelete('cascade');
$table->index(['category_id'], 'publication_categories_category_id');
$table->foreign('publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->index(['publication_id'], 'publication_categories_publication_id');
});
Schema::table('genres', function (Blueprint $table) {
$table->foreign('context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->index(['context_id'], 'genres_context_id');
});
Schema::table('genre_settings', function (Blueprint $table) {
$table->foreign('genre_id')->references('genre_id')->on('genres')->onDelete('cascade');
});
Schema::table('controlled_vocab_entries', function (Blueprint $table) {
$table->foreign('controlled_vocab_id')->references('controlled_vocab_id')->on('controlled_vocabs')->onDelete('cascade');
});
Schema::table('controlled_vocab_entry_settings', function (Blueprint $table) {
$table->foreign('controlled_vocab_entry_id', 'c_v_e_s_entry_id')->references('controlled_vocab_entry_id')->on('controlled_vocab_entries')->onDelete('cascade');
});
Schema::table('user_interests', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'user_interests_user_id');
$table->foreign('controlled_vocab_entry_id')->references('controlled_vocab_entry_id')->on('controlled_vocab_entries')->onDelete('cascade');
$table->index(['controlled_vocab_entry_id'], 'user_interests_controlled_vocab_entry_id');
});
Schema::table('user_settings', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
});
Schema::table('sessions', function (Blueprint $table) {
$table->foreign('user_id', 'sessions_user_id')->references('user_id')->on('users')->onDelete('cascade');
});
Schema::table('access_keys', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'access_keys_user_id');
});
Schema::table('notification_subscription_settings', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notification_subscription_settings_user_id');
$table->foreign('context')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->index(['context'], 'notification_subscription_settings_context');
});
Schema::table('email_templates', function (Blueprint $table) {
$table->foreign('context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->index(['context_id'], 'email_templates_context_id');
});
Schema::table('email_templates_settings', function (Blueprint $table) {
$table->foreign('email_id', 'email_templates_settings_email_id')->references('email_id')->on('email_templates')->onDelete('cascade');
$table->index(['email_id'], 'email_templates_settings_email_id');
});
// Permit nullable submission_ids where previously 0 was used for context library
Schema::table('library_files', function (Blueprint $table) {
$table->bigInteger('submission_id')->nullable()->change();
});
DB::table('library_files')->where('submission_id', '=', 0)->update(['submission_id' => null]);
Schema::table('library_files', function (Blueprint $table) {
$table->foreign('context_id', 'library_files_context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('library_file_settings', function (Blueprint $table) {
$table->foreign('file_id')->references('file_id')->on('library_files')->onDelete('cascade');
});
Schema::table('event_log', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'event_log_user_id');
});
Schema::table('event_log_settings', function (Blueprint $table) {
$table->foreign('log_id', 'event_log_settings_log_id')->references('log_id')->on('event_log')->onDelete('cascade');
});
Schema::table('email_log_users', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'email_log_users_user_id');
$table->foreign('email_log_id')->references('log_id')->on('email_log')->onDelete('cascade');
$table->index(['email_log_id'], 'email_log_users_email_log_id');
});
Schema::table('citations', function (Blueprint $table) {
$table->foreign('publication_id', 'citations_publication')->references('publication_id')->on('publications')->onDelete('cascade');
});
Schema::table('citation_settings', function (Blueprint $table) {
$table->foreign('citation_id', 'citation_settings_citation_id')->references('citation_id')->on('citations')->onDelete('cascade');
});
Schema::table('filters', function (Blueprint $table) {
$table->foreign('filter_group_id')->references('filter_group_id')->on('filter_groups')->onDelete('cascade');
$table->index(['filter_group_id'], 'filters_filter_group_id');
});
Schema::table('filter_settings', function (Blueprint $table) {
$table->foreign('filter_id')->references('filter_id')->on('filters')->onDelete('cascade');
});
Schema::table('temporary_files', function (Blueprint $table) {
$table->foreign('user_id', 'temporary_files_user_id')->references('user_id')->on('users')->onDelete('cascade');
});
Schema::table('notes', function (Blueprint $table) {
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notes_user_id');
});
Schema::table('navigation_menu_item_settings', function (Blueprint $table) {
$table->foreign('navigation_menu_item_id', 'navigation_menu_item_settings_navigation_menu_id')->references('navigation_menu_item_id')->on('navigation_menu_items')->onDelete('cascade');
});
Schema::table('navigation_menu_item_assignments', function (Blueprint $table) {
$table->foreign('navigation_menu_id')->references('navigation_menu_id')->on('navigation_menus')->onDelete('cascade');
$table->index(['navigation_menu_id'], 'navigation_menu_item_assignments_navigation_menu_id');
$table->foreign('navigation_menu_item_id')->references('navigation_menu_item_id')->on('navigation_menu_items')->onDelete('cascade');
$table->index(['navigation_menu_item_id'], 'navigation_menu_item_assignments_navigation_menu_item_id');
});
Schema::table('navigation_menu_item_assignment_settings', function (Blueprint $table) {
$table->foreign('navigation_menu_item_assignment_id', 'assignment_settings_navigation_menu_item_assignment_id')->references('navigation_menu_item_assignment_id')->on('navigation_menu_item_assignments')->onDelete('cascade');
});
if (Schema::hasTable('review_form_elements')) {
Schema::table('review_form_elements', function (Blueprint $table) {
$table->foreign('review_form_id', 'review_form_elements_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('cascade');
});
}
if (Schema::hasTable('review_form_settings')) {
Schema::table('review_form_settings', function (Blueprint $table) {
$table->foreign('review_form_id', 'review_form_settings_review_form_id')->references('review_form_id')->on('review_forms')->onDelete('cascade');
});
}
if (Schema::hasTable('review_form_element_settings')) {
Schema::table('review_form_element_settings', function (Blueprint $table) {
$table->foreign('review_form_element_id', 'review_form_element_settings_review_form_element_id')->references('review_form_element_id')->on('review_form_elements')->onDelete('cascade');
});
}
if (Schema::hasTable('review_form_responses')) {
Schema::table('review_form_responses', function (Blueprint $table) {
$table->foreign('review_form_element_id')->references('review_form_element_id')->on('review_form_elements')->onDelete('cascade');
$table->foreign('review_id')->references('review_id')->on('review_assignments')->onDelete('cascade');
$table->index(['review_id'], 'review_form_responses_review_id');
});
}
// The submissions_publication_id index was created for <3.3.0 but may be incorrect.
$schemaManager = DB::getDoctrineSchemaManager();
if (in_array('submissions_publication_id', array_keys($schemaManager->listTableIndexes('submissions')))) {
Schema::table('submissions', function (Blueprint $table) {
$table->dropIndex('submissions_publication_id');
});
}
Schema::table('submissions', function (Blueprint $table) {
$table->foreign('context_id', 'submissions_context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->foreign('current_publication_id', 'submissions_publication_id')->references('publication_id')->on('publications')->onDelete('set null');
});
Schema::table('submission_settings', function (Blueprint $table) {
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('publication_settings', function (Blueprint $table) {
$table->foreign('publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
});
Schema::table('authors', function (Blueprint $table) {
$table->foreign('publication_id')->references('publication_id')->on('publications')->onDelete('cascade');
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups');
$table->index(['user_group_id'], 'authors_user_group_id');
});
Schema::table('author_settings', function (Blueprint $table) {
$table->foreign('author_id', 'author_settings_author_id')->references('author_id')->on('authors')->onDelete('cascade');
});
Schema::table('edit_decisions', function (Blueprint $table) {
$table->foreign('editor_id', 'edit_decisions_editor_id')->references('user_id')->on('users')->onDelete('cascade');
$table->foreign('submission_id', 'edit_decisions_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('submission_comments', function (Blueprint $table) {
$table->foreign('submission_id', 'submission_comments_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->foreign('author_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['author_id'], 'submission_comments_author_id');
});
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->foreign('context_id', 'section_editors_context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->foreign('user_id', 'subeditor_submission_group_user_id')->references('user_id')->on('users')->onDelete('cascade');
});
Schema::table('submission_search_objects', function (Blueprint $table) {
$table->foreign('submission_id', 'submission_search_object_submission')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('submission_search_object_keywords', function (Blueprint $table) {
$table->foreign('object_id')->references('object_id')->on('submission_search_objects')->onDelete('cascade');
$table->foreign('keyword_id', 'submission_search_object_keywords_keyword_id')->references('keyword_id')->on('submission_search_keyword_list')->onDelete('cascade');
});
Schema::table('review_rounds', function (Blueprint $table) {
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('review_round_files', function (Blueprint $table) {
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds')->onDelete('cascade');
$table->index(['review_round_id'], 'review_round_files_review_round_id');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->index(['submission_file_id'], 'review_round_files_submission_file_id');
});
Schema::table('user_user_groups', function (Blueprint $table) {
$table->foreign('user_id', 'user_user_groups_user_id')->references('user_id')->on('users')->onDelete('cascade');
});
Schema::table('user_group_stage', function (Blueprint $table) {
$table->foreign('context_id', 'user_group_stage_context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
});
Schema::table('stage_assignments', function (Blueprint $table) {
$table->foreign('user_group_id', 'stage_assignments_user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->foreign('user_id', 'stage_assignments_user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
});
Schema::table('submission_files', function (Blueprint $table) {
$table->foreign('submission_id', 'submission_files_submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
$table->foreign('genre_id')->references('genre_id')->on('genres')->onDelete('set null');
$table->index(['genre_id'], 'submission_files_genre_id');
$table->index(['file_id'], 'submission_files_file_id');
$table->foreign('uploader_user_id')->references('user_id')->on('users')->onDelete('set null');
$table->index(['uploader_user_id'], 'submission_files_uploader_user_id');
$table->bigInteger('source_submission_file_id')->unsigned()->nullable()->change();
$table->foreign('source_submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['source_submission_file_id'], 'submission_files_source_submission_file_id');
});
Schema::table('data_object_tombstone_settings', function (Blueprint $table) {
$table->foreign('tombstone_id', 'data_object_tombstone_settings_tombstone_id')->references('tombstone_id')->on('data_object_tombstones')->onDelete('cascade');
});
Schema::table('data_object_tombstone_oai_set_objects', function (Blueprint $table) {
$table->foreign('tombstone_id', 'data_object_tombstone_oai_set_objects_tombstone_id')->references('tombstone_id')->on('data_object_tombstones')->onDelete('cascade');
});
Schema::table('submission_file_revisions', function (Blueprint $table) {
$table->index(['submission_file_id'], 'submission_file_revisions_submission_file_id');
$table->index(['file_id'], 'submission_file_revisions_file_id');
});
Schema::table($this->getContextSettingsTable(), function (Blueprint $table) {
$table->foreign($this->getContextKeyField(), $this->getContextSettingsTable() . '_' . $this->getContextKeyField())->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
});
Schema::table('review_files', function (Blueprint $table) {
$table->foreign('review_id')->references('review_id')->on('review_assignments')->onDelete('cascade');
});
Schema::table('notifications', function (Blueprint $table) {
$table->bigInteger('context_id')->nullable()->change();
$table->bigInteger('user_id')->nullable()->change();
});
DB::table('notifications')->where('context_id', '=', 0)->update(['context_id' => null]);
DB::table('notifications')->where('user_id', '=', 0)->update(['user_id' => null]);
Schema::table('notifications', function (Blueprint $table) {
// Renaming indexes directly not supported until MariaDB 10.5.2
$table->dropIndex('notifications_context_id');
$table->index(['context_id', 'level'], 'notifications_context_id_level');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'notifications_user_id');
$table->foreign('context_id')->references($this->getContextKeyField())->on($this->getContextTable())->onDelete('cascade');
$table->index(['context_id'], 'notifications_context_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,35 @@
<?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 Add a required column to the `genres` table (file types)
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class I6241_RequiredGenres extends \PKP\migration\Migration
{
public function up(): void
{
Schema::table('genres', function (Blueprint $table) {
$table->smallInteger('required')->default(0);
});
}
public function down(): void
{
Schema::table('genres', function (Blueprint $table) {
$table->dropColumn('required');
});
}
}
@@ -0,0 +1,54 @@
<?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
*
* @brief Set the new context setting,`submitWithCategories`, to `true` for
* existing journals to preserve the pre-existing behaviour.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\migration\Migration;
abstract class I6306_EnableCategories extends Migration
{
abstract protected function getContextTable(): string;
abstract protected function getContextSettingsTable(): string;
abstract protected function getContextIdColumn(): string;
public function up(): void
{
$contextIds = DB::table($this->getContextTable())
->get([$this->getContextIdColumn()])
->pluck($this->getContextIdColumn());
if (!$contextIds->count()) {
return;
}
DB::table($this->getContextSettingsTable())->insert(
$contextIds->map(fn (int $id) => [
$this->getContextIdColumn() => $id,
'setting_name' => 'submitWithCategories',
'setting_value' => '1',
])
->toArray()
);
}
public function down(): void
{
DB::table($this->getContextSettingsTable())
->where('setting_name', 'submitWithCategories')
->delete();
}
}
@@ -0,0 +1,68 @@
<?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 given metric type 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 PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class I6782_CleanOldMetrics extends Migration
{
abstract protected function getMetricType(): string;
/**
* Run the migration.
*/
public function up(): void
{
// Delete the entries with the metric type ojs::counter from the DB table metrics -> they were migrated in earlier scripts
if (Schema::hasTable('metrics')) {
DB::table('metrics')->where('metric_type', '=', $this->getMetricType())->delete();
// Move back the orphaned metrics form the temporary metrics_tmp
$metricsColumns = Schema::getColumnListing('metrics_tmp');
$metricsTmp = DB::table('metrics_tmp')->select($metricsColumns);
DB::table('metrics')->insertUsing($metricsColumns, $metricsTmp);
Schema::drop('metrics_tmp');
$metricsExist = DB::table('metrics')->count();
// if table metrics is now not empty rename it, else delete it
if ($metricsExist > 0) {
Schema::rename('metrics', 'metrics_old');
} else {
Schema::drop('metrics');
}
}
// Delete the old usage_stats_temporary_records table
if (Schema::hasTable('usage_stats_temporary_records')) {
Schema::drop('usage_stats_temporary_records');
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,54 @@
<?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 PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class I6782_MetricsContext extends Migration
{
abstract protected function getMetricType(): string;
abstract protected function getContextAssocType(): int;
/**
* 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
$selectContextMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.assoc_id, {$dayFormatSql}, m.metric"))
->where('m.assoc_type', '=', $this->getContextAssocType())
->where('m.metric_type', '=', $this->getMetricType());
DB::table('metrics_context')->insertUsing(['load_id', 'context_id', 'date', 'metric'], $selectContextMetrics);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,88 @@
<?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 PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class I6782_MetricsGeo extends Migration
{
private const ASSOC_TYPE_SUBMISSION = 0x0100009;
private const ASSOC_TYPE_SUBMISSION_FILE = 0x0000203;
private const ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 0x0000213;
abstract protected function getMetricType(): string;
/**
* 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 Geo metrics -- no matter if the Geo usage stats are currently enabled
// fix wrong entries in the DB table metrics
// do all this first in order for groupBy to function properly
DB::table('metrics')->where('city', '')->update(['city' => null]);
DB::table('metrics')->where('region', '')->orWhere('region', '0')->update(['region' => null]);
DB::table('metrics')->where('country_id', '')->update(['country_id' => null]);
// in the GeoIP Legacy databases, several country codes were included that don't represent countries
DB::table('metrics')->whereIn('country_id', ['AP', 'EU', 'A1', 'A2'])->update(['country_id' => null, 'region' => null, 'city' => null]);
// some regions are missing the leading '0'
DB::table('metrics')->update(['region' => DB::raw("LPAD(region, 2, '0')")]);
// Insert into metrics_submission_geo_daily table
// metric = total views = abstracts + galley and supp files
$selectGeoMetrics = DB::table('metrics as m')
->select(DB::raw("m.load_id, m.context_id, m.submission_id, COALESCE(m.country_id, ''), COALESCE(m.region, ''), COALESCE(m.city, ''), {$dayFormatSql} as mday, SUM(m.metric), 0"))
->whereIn('m.assoc_type', [self::ASSOC_TYPE_SUBMISSION, self::ASSOC_TYPE_SUBMISSION_FILE, self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER])
->where('m.metric_type', '=', $this->getMetricType())
->where(function ($q) {
$q->whereNotNull('m.country_id')
->orWhereNotNull('m.region')
->orWhereNotNull('m.city');
})
->groupBy(DB::raw('m.load_id, m.context_id, m.submission_id, m.country_id, m.region, m.city, mday'));
DB::table('metrics_submission_geo_daily')->insertUsing(['load_id', 'context_id', 'submission_id', 'country', 'region', 'city', 'date', 'metric', 'metric_unique'], $selectGeoMetrics);
// Migrate to metrics_submission_geo_monthly table
$monthFormatSql = "CAST(DATE_FORMAT(gd.date, '%Y%m') AS UNSIGNED)";
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
$monthFormatSql = "to_char(gd.date, 'YYYYMM')::integer";
}
// use the table metrics_submission_geo_daily instead of table metrics to calculate the monthly numbers
$selectSubmissionGeoDaily = DB::table('metrics_submission_geo_daily as gd')
->select(DB::raw("gd.context_id, gd.submission_id, COALESCE(gd.country, ''), COALESCE(gd.region, ''), COALESCE(gd.city, ''), {$monthFormatSql} as gdmonth, SUM(gd.metric), SUM(gd.metric_unique)"))
->groupBy(DB::raw('gd.context_id, gd.submission_id, gd.country, gd.region, gd.city, gdmonth'));
DB::table('metrics_submission_geo_monthly')->insertUsing(['context_id', 'submission_id', 'country', 'region', 'city', 'month', 'metric', 'metric_unique'], $selectSubmissionGeoDaily);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,147 @@
<?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/ops/omp::counter here, because these entries will be removed during the upgrade.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class I6782_OrphanedMetrics extends Migration
{
private const ASSOC_TYPE_SUBMISSION = 0x0100009;
private const ASSOC_TYPE_SUBMISSION_FILE = 0x0000203;
private const ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 0x0000213;
abstract protected function getMetricType(): string;
abstract protected function getContextAssocType(): int;
abstract protected function getContextTable(): string;
abstract protected function getContextKeyField(): string;
abstract protected function getRepresentationTable(): string;
abstract protected function getRepresentationKeyField(): string;
/**
* Get assoc types that will be considered in the upgrade
*/
protected function getAssocTypesToMigrate(): array
{
return [
self::ASSOC_TYPE_SUBMISSION,
self::ASSOC_TYPE_SUBMISSION_FILE,
self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER,
];
}
/**
* 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 upgrade
*/
public function up(): void
{
$this->createMetricsTmpTable();
$metricsColumns = Schema::getColumnListing('metrics_tmp');
// Clean orphaned metrics context IDs. These IDs can be deleted.
// as context_id
$orphanedIds = DB::table('metrics AS m')->leftJoin($this->getContextTable() . ' AS c', 'm.context_id', '=', 'c.' . $this->getContextKeyField())->where('m.metric_type', '=', $this->getMetricType())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('m.context_id');
foreach ($orphanedIds as $contextId) {
$this->_installer->log("Removing stats for context {$contextId} because no context with that ID could be found.");
DB::table('metrics')->where('context_id', '=', $contextId)->delete();
}
// as assoc_id
$orphanedIds = DB::table('metrics AS m')->leftJoin($this->getContextTable() . ' AS c', 'm.assoc_id', '=', 'c.' . $this->getContextKeyField())->where('m.assoc_type', '=', $this->getContextAssocType())->where('m.metric_type', '=', $this->getMetricType())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('m.assoc_id');
foreach ($orphanedIds as $contextId) {
$this->_installer->log("Removing stats for context {$contextId} because no context with that ID could be found.");
DB::table('metrics')->where('assoc_type', '=', $this->getContextAssocType())->where('assoc_id', '=', $contextId)->delete();
}
// Clean orphaned metrics submission IDs
// as submission_id
$orphanedIds = DB::table('metrics AS m')->leftJoin('submissions AS s', 'm.submission_id', '=', 's.submission_id')->whereNotNull('m.submission_id')->whereNull('s.submission_id')->distinct()->pluck('m.submission_id');
$orphandedSubmissions = DB::table('metrics')->select($metricsColumns)->whereIn('submission_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedSubmissions);
DB::table('metrics')->whereIn('submission_id', $orphanedIds)->delete();
// as assoc_id
$orphanedIds = DB::table('metrics AS m')->leftJoin('submissions AS s', 'm.assoc_id', '=', 's.submission_id')->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION)->whereNull('s.submission_id')->distinct()->pluck('m.assoc_id');
$orphandedSubmissionsAssocId = DB::table('metrics')->select($metricsColumns)->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION)->whereIn('assoc_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedSubmissionsAssocId);
DB::table('metrics')->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION)->whereIn('assoc_id', $orphanedIds)->delete();
// Clean orphaned metrics submission file IDs
$orphanedIds = DB::table('metrics AS m')->leftJoin('submission_files AS sf', 'm.assoc_id', '=', 'sf.submission_file_id')->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE)->whereNull('sf.submission_file_id')->distinct()->pluck('m.assoc_id');
$orphandedSubmissionFiles = DB::table('metrics')->select($metricsColumns)->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE)->whereIn('assoc_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedSubmissionFiles);
DB::table('metrics')->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE)->whereIn('assoc_id', $orphanedIds)->delete();
// Clean orphaned metrics submission supp file IDs
$orphanedIds = DB::table('metrics AS m')->leftJoin('submission_files AS sf', 'm.assoc_id', '=', 'sf.submission_file_id')->where('m.assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER)->whereNull('sf.submission_file_id')->distinct()->pluck('m.assoc_id');
$orphandedSubmissionSuppFiles = DB::table('metrics')->select($metricsColumns)->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER)->whereIn('assoc_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedSubmissionSuppFiles);
DB::table('metrics')->where('assoc_type', '=', self::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER)->whereIn('assoc_id', $orphanedIds)->delete();
// Clean orphaned metrics representation IDs
$orphanedIds = DB::table('metrics AS m')->leftJoin($this->getRepresentationTable() . ' AS r', 'm.representation_id', '=', 'r.' . $this->getRepresentationKeyField())->whereNotNull('m.representation_id')->whereNull('r.' . $this->getRepresentationKeyField())->distinct()->pluck('m.representation_id');
$orphandedRepresentations = DB::table('metrics')->select($metricsColumns)->whereIn('representation_id', $orphanedIds)->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphandedRepresentations);
DB::table('metrics')->whereIn('representation_id', $orphanedIds)->delete();
// Copy assoc types that will not be migrated to the metrics_tmp table
$orphanedAssocTypes = DB::table('metrics AS m')->select($metricsColumns)->whereNotIn('m.assoc_type', $this->getAssocTypesToMigrate())->where('metric_type', '=', $this->getMetricType());
DB::table('metrics_tmp')->insertUsing($metricsColumns, $orphanedAssocTypes);
}
public function createMetricsTmpTable()
{
Schema::create('metrics_tmp', function (Blueprint $table) {
$table->string('load_id', 255);
$table->bigInteger('context_id');
$table->bigInteger('pkp_section_id')->nullable();
$table->bigInteger('assoc_object_type')->nullable();
$table->bigInteger('assoc_object_id')->nullable();
$table->bigInteger('submission_id')->nullable();
$table->bigInteger('representation_id')->nullable();
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->string('day', 8)->nullable();
$table->string('month', 6)->nullable();
$table->smallInteger('file_type')->nullable();
$table->string('country_id', 2)->nullable();
$table->string('region', 2)->nullable();
$table->string('city', 255)->nullable();
$table->string('metric_type', 255);
$table->integer('metric');
$table->index(['load_id'], 'metrics_tmp_load_id');
$table->index(['metric_type', 'context_id'], 'metrics_tmp_metric_type_context_id');
$table->index(['metric_type', 'submission_id', 'assoc_type'], 'metrics_tmp_metric_type_submission_id_assoc_type');
$table->index(['metric_type', 'context_id', 'assoc_type', 'assoc_id'], 'metrics_tmp_metric_type_submission_id_assoc');
});
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,137 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6782_UsageStatsSettings.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_UsageStatsSettings
*
* @brief Migrate usage stats settings.
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Services;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
use PKP\plugins\PluginRegistry;
class I6782_UsageStatsSettings extends Migration
{
/**
* Run the migration.
*/
public function up(): void
{
// Read old usage stats settings
// Geo data stats settings
$optionalColumns = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'optionalColumns')
->value('setting_value');
$enableGeoUsageStats = 'disabled';
$keepDailyUsageStats = false;
if (!is_null($optionalColumns)) {
$keepDailyUsageStats = true;
if (str_contains($optionalColumns, 'city')) {
$enableGeoUsageStats = 'country+region+city';
} elseif (str_contains($optionalColumns, 'region')) {
$enableGeoUsageStats = 'country+region';
} else {
$enableGeoUsageStats = 'country';
}
}
// Compress archives settings
$compressArchives = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'compressArchives')
->value('setting_value');
$compressStatsLogs = !is_null($compressArchives) ? $compressArchives : false;
// Other, default site settings
$enableInstitutionUsageStats = $isSiteSushiPlatform = false;
$isSushiApiPublic = true;
// Migrate site settings
DB::table('site_settings')->insertOrIgnore([
['setting_name' => 'compressStatsLogs', 'setting_value' => $compressStatsLogs],
['setting_name' => 'enableGeoUsageStats', 'setting_value' => $enableGeoUsageStats],
['setting_name' => 'keepDailyUsageStats', 'setting_value' => $keepDailyUsageStats],
['setting_name' => 'enableInstitutionUsageStats', 'setting_value' => $enableInstitutionUsageStats],
['setting_name' => 'isSushiApiPublic', 'setting_value' => $isSushiApiPublic],
['setting_name' => 'isSiteSushiPlatform', 'setting_value' => $isSiteSushiPlatform]
]);
// Display site settings
$displayStatistics = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'displayStatistics')
->where('context_id', '=', 0)
->value('setting_value');
$chartType = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'chartType')
->where('context_id', '=', 0)
->value('setting_value');
// Migrate usage stats site display settings to the active site theme
$siteThemePlugins = PluginRegistry::getPlugins('themes');
$activeSiteTheme = null;
foreach ($siteThemePlugins as $siteThemePlugin) {
if ($siteThemePlugin->isActive()) {
$activeSiteTheme = $siteThemePlugin;
break;
}
}
if (isset($activeSiteTheme)) {
$siteUsageStatsDisplay = !$displayStatistics ? 'none' : $chartType;
DB::table('plugin_settings')->insertOrIgnore([
['plugin_name' => $activeSiteTheme->getName(), 'context_id' => 0, 'setting_name' => 'displayStats', 'setting_value' => $siteUsageStatsDisplay, 'setting_type' => 'string'],
]);
}
// Migrate context settings
// Get all, also disabled, contexts
$contextIds = Services::get('context')->getIds();
foreach ($contextIds as $contextId) {
$contextDisplayStatistics = $contextChartType = null;
$contextDisplayStatistics = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'displayStatistics')
->where('context_id', '=', $contextId)
->value('setting_value');
$contextChartType = DB::table('plugin_settings')
->where('plugin_name', '=', 'usagestatsplugin')
->where('setting_name', '=', 'chartType')
->where('context_id', '=', $contextId)
->value('setting_value');
// Migrate usage stats display settings to the active context theme
$contextThemePlugins = PluginRegistry::loadCategory('themes', true, $contextId);
$activeContextTheme = null;
foreach ($contextThemePlugins as $contextThemePlugin) {
if ($contextThemePlugin->isActive()) {
$activeContextTheme = $contextThemePlugin;
break;
}
}
if (isset($activeContextTheme)) {
$contextUsageStatsDisplay = !$contextDisplayStatistics ? 'none' : $contextChartType;
DB::table('plugin_settings')->insertOrIgnore([
['plugin_name' => $activeContextTheme->getName(), 'context_id' => $contextId, 'setting_name' => 'displayStats', 'setting_value' => $contextUsageStatsDisplay, 'setting_type' => 'string'],
]);
}
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,85 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I6895_CreateNewInstitutionsTables.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 I6895_CreateNewInstitutionsTables
*
* @brief Describe database table structures.
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Application;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema as Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I6895_CreateNewInstitutionsTables extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Institutions.
Schema::create('institutions', function (Blueprint $table) {
$table->bigInteger('institution_id')->autoIncrement();
$table->bigInteger('context_id');
$contextDao = Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'institutions_context_id');
$table->string('ror', 255)->nullable();
$table->softDeletes('deleted_at', 0);
});
// Locale-specific institution data
Schema::create('institution_settings', function (Blueprint $table) {
$table->bigIncrements('institution_setting_id');
$table->bigInteger('institution_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->foreign('institution_id')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'institution_settings_institution_id');
$table->unique(['institution_id', 'locale', 'setting_name'], 'institution_settings_unique');
});
// Institution IPs and IP ranges.
Schema::create('institution_ip', function (Blueprint $table) {
$table->bigInteger('institution_ip_id')->autoIncrement();
$table->bigInteger('institution_id');
$table->string('ip_string', 40);
$table->bigInteger('ip_start');
$table->bigInteger('ip_end')->nullable();
$table->foreign('institution_id')->references('institution_id')->on('institutions')->onDelete('cascade');
$table->index(['institution_id'], 'institution_ip_institution_id');
$table->index(['ip_start'], 'institution_ip_start');
$table->index(['ip_end'], 'institution_ip_end');
});
if (Schema::hasTable('institutional_subscriptions') && Schema::hasColumn('institutional_subscriptions', 'institution_id')) {
Schema::table('institutional_subscriptions', function (Blueprint $table) {
$table->foreign('institution_id')->references('institution_id')->on('institutions');
$table->index(['institution_id'], 'institutional_subscriptions_institution_ip');
});
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7126_Galleys.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 I7126_Galleys
*
* @brief Update the galley class filters in the database
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Application;
use Illuminate\Support\Facades\DB;
class I7126_Galleys extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('filter_groups')
->where('input_type', 'class::classes.article.ArticleGalley')
->orWhere('input_type', 'class::classes.preprint.PreprintGalley')
->update(['input_type' => 'class::lib.pkp.classes.galley.Galley']);
DB::table('filter_groups')
->where('output_type', 'class::classes.article.ArticleGalley')
->orWhere('output_type', 'class::classes.preprint.PreprintGalley')
->update(['output_type' => 'class::lib.pkp.classes.galley.Galley']);
}
/**
* Reverse the upgrades
*/
public function down(): void
{
if (Application::get()->getName() === 'ojs2') {
$class = 'class::classes.article.ArticleGalley';
} elseif (Application::get()->getName() === 'ops') {
$class = 'class::classes.preprint.PreprintGalley';
} else {
// Nothing to revert
return;
}
DB::table('filter_groups')
->where('input_type', 'class::lib.pkp.classes.galley.Galley')
->update(['input_type' => $class]);
DB::table('filter_groups')
->where('output_type', 'class::lib.pkp.classes.galley.Galley')
->update(['input_type' => $class]);
}
}
@@ -0,0 +1,45 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7190_UpdateFilters.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 I7190_UpdateFilters
*
* @brief Ensures filters based on the NativeImportFilter expect an array as output.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I7190_UpdateFilters extends Migration
{
/**
* @inheritDoc
*/
public function up(): void
{
DB::update(
"UPDATE filter_groups
SET output_type = CONCAT(output_type, '[]')
WHERE
symbolic LIKE 'native-xml=>%'
AND output_type LIKE 'class::%'
AND output_type NOT LIKE '%[]'"
);
}
/**
* @inheritDoc
*/
public function down(): void
{
throw new DowngradeNotSupportedException('Downgrade unsupported due to updated data');
}
}
@@ -0,0 +1,151 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7191_EditorAssignments.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 I7191_EditorAssignments
*
* @brief Update the subeditor_submission_group table to accomodate new editor assignment settings
*/
namespace PKP\migration\upgrade\v3_4_0;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
abstract class I7191_EditorAssignments extends \PKP\migration\Migration
{
protected string $sectionDb;
protected string $sectionIdColumn;
protected string $contextColumn;
/**
* Adds a user_group_id column to the subeditor_submission_group
* table and adds initial data. Adds foreign keys where appropriate.
*/
public function up(): void
{
if (empty($this->sectionDb) || empty($this->sectionIdColumn) || empty($this->contextColumn)) {
throw new Exception('Upgrade could not be completed because required properties for the I7191_EditorAssignments migration are undefined.');
}
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->bigInteger('user_group_id')->nullable();
});
$this->setUserGroup();
$this->deleteOrphanedAssignments();
$this->addAutomatedAssignments();
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->bigInteger('user_group_id')->nullable(false)->change();
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
$table->index(['user_group_id'], 'subeditor_submission_group_user_group_id');
});
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->dropForeign('subeditor_submission_group_user_group_id_foreign');
$table->dropColumn('user_group_id');
});
}
/**
* Set the user group for all existing assignments
*
* Uses the default user group for Role::ROLE_ID_SUB_EDITOR
* if one exists
*/
protected function setUserGroup(): void
{
$row = DB::table('user_groups')
->where('role_id', '=', 17) // Role::ROLE_ID_SUB_EDITOR
->orderBy('is_default')
->get(['user_group_id', 'context_id'])
->first();
if ($row) {
DB::table('subeditor_submission_group')
->whereNull('user_group_id')
->where('context_id', '=', $row->context_id)
->update(['user_group_id' => $row->user_group_id]);
}
}
/**
* Delete any orphaned records
*
* 1. Editorial assignments without a user group. They wouldn't have been
* assigned anyway so the cleanup should not impact existing workflows.
* 2. Editorial assignments without a matching user record.
*/
protected function deleteOrphanedAssignments(): void
{
DB::table('subeditor_submission_group')
->whereNull('user_group_id')
->delete();
DB::table('subeditor_submission_group as ssg')
->leftJoin('users as u', 'u.user_id', '=', 'ssg.user_id')
->whereNull('u.user_id')
->delete();
}
/**
* Add assignment records for any editors who would have
* been automatically assigned in previous versions.
*
* When a context had only one user in a user group with
* the following roles, that user would automatically be
* assigned to every submission.
*
* Role::ROLE_ID_MANAGER, Role::ROLE_ID_ASSISTANT
*
* This migration assigns such users to every section in
* the context
*/
protected function addAutomatedAssignments(): void
{
$userGroups = DB::table('user_groups')
->whereIn('role_id', [16, 4097]) // [Role::ROLE_ID_MANAGER, Role::ROLE_ID_ASSISTANT]
->get(['user_group_id', 'context_id']);
$userGroups->each(function ($userGroup) {
$userIds = DB::table('user_user_groups')
->where('user_group_id', '=', $userGroup->user_group_id)
->pluck('user_id');
if ($userIds->count() !== 1) {
return;
}
$newRows = DB::table($this->sectionDb)
->where($this->contextColumn, '=', $userGroup->context_id)
->pluck($this->sectionIdColumn)
->map(function (int $sectionId) use ($userGroup, $userIds) {
return [
'context_id' => $userGroup->context_id,
'assoc_type' => 530, // ASSOC_TYPE_SECTION
'assoc_id' => $sectionId,
'user_id' => $userIds->first(),
'user_group_id' => $userGroup->user_group_id,
];
});
if ($newRows->count()) {
DB::table('subeditor_submission_group')->insertOrIgnore($newRows->toArray());
}
});
}
}
@@ -0,0 +1,138 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7191_InstallSubmissionHelpDefaults.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 I7191_InstallSubmissionHelpDefaults
*
* @brief Install new localized defaults for submission help text
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Application;
use Exception;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use PKP\facades\Locale;
use stdClass;
abstract class I7191_InstallSubmissionHelpDefaults extends \PKP\migration\Migration
{
protected string $CONTEXT_TABLE = '';
protected string $CONTEXT_SETTINGS_TABLE = '';
protected string $CONTEXT_COLUMN = '';
public function up(): void
{
if (empty($this->CONTEXT_TABLE) || empty($this->CONTEXT_SETTINGS_TABLE) || empty($this->CONTEXT_COLUMN)) {
throw new Exception('Upgrade could not be completed because required properties for the I7191_InstallSubmissionHelpDefaults migration are undefined.');
}
$initialLocale = Locale::getLocale();
DB::table($this->CONTEXT_TABLE . ' as ct')
->leftJoin($this->CONTEXT_SETTINGS_TABLE . ' as cst', function (JoinClause $join) {
$join->on('ct.' . $this->CONTEXT_COLUMN, '=', 'cst.' . $this->CONTEXT_COLUMN)
->where('cst.setting_name', '=', 'supportedLocales');
})
->get([
'ct.' . $this->CONTEXT_COLUMN,
'ct.path',
'ct.primary_locale',
'cst.setting_value as supportedLocales'])
->each(function (stdClass $row) {
foreach (json_decode($row->supportedLocales) as $locale) {
Locale::setLocale($locale);
$localizationParams = $this->getLocalizationParams($row, $locale);
DB::table($this->CONTEXT_SETTINGS_TABLE)
->insert(
$this->getNewSettings()->map(
function ($localeKey, $settingName) use ($row, $locale, $localizationParams) {
return [
$this->CONTEXT_COLUMN => $row->{$this->CONTEXT_COLUMN},
'locale' => $locale,
'setting_name' => $settingName,
'setting_value' => __($localeKey, $localizationParams),
];
}
)->toArray()
);
}
});
Locale::setLocale($initialLocale);
}
public function down(): void
{
DB::table($this->CONTEXT_SETTINGS_TABLE)
->whereIn('setting_name', $this->getNewSettings()->keys())
->delete();
}
/**
* @return Collection [settingName => localeKey]
*/
protected function getNewSettings(): Collection
{
return collect([
'beginSubmissionHelp' => 'default.submission.step.beforeYouBegin',
'contributorsHelp' => 'default.submission.step.contributors',
'detailsHelp' => 'default.submission.step.details',
'forTheEditorsHelp' => 'default.submission.step.forTheEditors',
'reviewHelp' => 'default.submission.step.review',
'uploadFilesHelp' => 'default.submission.step.uploadFiles',
]);
}
/**
* Get the params needed to localize some of the default settings
*/
protected function getLocalizationParams(stdClass $row, string $locale): array
{
return [
'submissionGuidelinesUrl' => Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$row->path,
'about',
'submissions'
),
'contextName' => $this->getContextName($row->{$this->CONTEXT_COLUMN}, $locale, $row->primary_locale),
];
}
/**
* Get the name of a context
*
* Sorts all names with preferred locale first and primary locale
* second. Chooses top name after the sort.
*/
protected function getContextName(int $contextId, string $preferredLocale, string $primaryLocale): string
{
$rows = DB::table($this->CONTEXT_SETTINGS_TABLE)
->where($this->CONTEXT_COLUMN, $contextId)
->where('setting_name', 'name')
->whereNotNull('setting_value')
->get(['locale', 'setting_value'])
->toArray();
if (empty($rows)) {
throw new Exception('Upgrade failed because no name was found for context ' . $contextId);
}
usort($rows, function ($a, $b) use ($preferredLocale, $primaryLocale) {
return $a->locale === $preferredLocale
|| ($a->locale === $primaryLocale && $b->locale !== $preferredLocale);
});
return $rows[0]->setting_value;
}
}
@@ -0,0 +1,80 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7191_ResubscribeSubeditors.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 I7191_ResubscribeSubeditors
*
* @brief Resubscribe subeditors to certain email notifications.
*
* In previous versions, subeditors received two emails
* with a new submission: NOTIFICATION_TYPE_SUBMISSION_SUBMITTED and a copy
* of the email sent to the author. The NOTIFICATION_TYPE_SUBMISSION_SUBMITTED
* email is more appropriate, but they could not opt out of the author copy, so
* many editors unsubscribed from the NOTIFICATION_TYPE_SUBMISSION_SUBMITTED
* email.
*
* In 3.4, this was fixed so that subeditors will not receive two emails.
* However, to ensure they continue receiving at least one email, we need to
* resubscribe them to the NOTIFICATION_TYPE_SUBMISSION_SUBMITTED email. If
* they want, they can unsubscribe again.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\DB;
class I7191_ResubscribeSubeditors extends \PKP\migration\Migration
{
/**
* PKP\notification\Notification::NOTIFICATION_TYPE_SUBMISSION_SUBMITTED
*/
public const NOTIFICATION_TYPE = 0x1000001;
/**
* PKP\security\Role::ROLE_ID_SUB_EDITOR
*/
public const SUBEDITOR_ROLE = 0x00000011;
protected Enumerable $removedRows;
public function up(): void
{
$this->removedRows = DB::table('notification_subscription_settings as nss')
->leftJoin('user_user_groups as uug', 'nss.user_id', '=', 'uug.user_id')
->leftJoin('user_groups as ug', function (JoinClause $join) {
$join->on('ug.user_group_id', '=', 'uug.user_group_id')
->on('ug.context_id', '=', 'nss.context')
->where('ug.role_id', '=', self::SUBEDITOR_ROLE);
})
->distinct()
->where('nss.setting_name', 'blocked_emailed_notification')
->where('nss.setting_value', self::NOTIFICATION_TYPE)
->whereNotNull('ug.user_group_id')
->get(['nss.*', 'nss.context']);
DB::table('notification_subscription_settings')
->whereIn(
'setting_id',
$this->removedRows->map(fn ($row) => $row->setting_id)
)
->delete();
}
public function down(): void
{
DB::table('notification_subscription_settings')
->insert(
$this
->removedRows
->map(fn ($row) => (array) $row)
->toArray()
);
}
}
@@ -0,0 +1,94 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7191_SubmissionChecklistMigration.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 I7191_SubmissionChecklistMigration
*
* @brief Migrate the submissionChecklist setting from an array to a HTML string
*/
namespace PKP\migration\upgrade\v3_4_0;
use Exception;
use Illuminate\Support\Facades\DB;
use PKP\facades\Locale;
use PKP\install\DowngradeNotSupportedException;
use stdClass;
abstract class I7191_SubmissionChecklistMigration extends \PKP\migration\Migration
{
protected string $CONTEXT_SETTINGS_TABLE = '';
protected string $CONTEXT_COLUMN = '';
/**
* Run the migrations.
*/
public function up(): void
{
if (empty($this->CONTEXT_SETTINGS_TABLE) || empty($this->CONTEXT_COLUMN)) {
throw new Exception('Upgrade could not be completed because required properties for the I7191_SubmissionChecklistMigration migration are undefined.');
}
$initialLocale = Locale::getLocale();
DB::table($this->CONTEXT_SETTINGS_TABLE)
->where('setting_name', 'submissionChecklist')
->get()
->each(function (stdClass $row) {
if (empty($row->setting_value)) {
DB::table($this->CONTEXT_SETTINGS_TABLE)
->where($this->CONTEXT_COLUMN, $row->{$this->CONTEXT_COLUMN})
->where('locale', $row->locale)
->where('setting_name', 'submissionChecklist')
->where('setting_value', $row->setting_value)
->delete();
return;
}
$checklist = json_decode($row->setting_value, true);
usort($checklist, fn ($a, $b) => ($a['order'] ?? 0) <=> ($b['order'] ?? 0));
$textList = [];
foreach ($checklist as $item) {
if (!isset($item['content'])) {
continue;
}
$textList[] = $item['content'];
}
Locale::setLocale($row->locale);
$newValue = '<p>'
. __('submission.submit.submissionChecklistDescription')
. '</p>'
. '<ul><li>'
. join('</li><li>', $textList)
. '</li></ul>';
DB::table($this->CONTEXT_SETTINGS_TABLE)
->where($this->CONTEXT_COLUMN, $row->{$this->CONTEXT_COLUMN})
->where('locale', $row->locale)
->where('setting_name', 'submissionChecklist')
->update([
'setting_value' => $newValue,
]);
});
Locale::setLocale($initialLocale);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7191_SubmissionProgressType.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 I7191_SubmissionProgressType
*
* @brief Change the submission_progress setting from an int to a string to match
* the new step ids
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class I7191_SubmissionProgressType extends \PKP\migration\Migration
{
public function up(): void
{
Schema::table('submissions', function (Blueprint $table) {
$table->string('submission_progress_temp', 50)->default('start');
});
foreach ($this->getStepMap() as $oldValue => $newValue) {
DB::table('submissions')
->where('submission_progress', $oldValue)
->update([
'submission_progress_temp' => $newValue,
]);
}
Schema::table('submissions', function (Blueprint $table) {
$table->dropColumn('submission_progress');
$table->renameColumn('submission_progress_temp', 'submission_progress');
});
}
public function down(): void
{
Schema::table('submissions', function (Blueprint $table) {
$table->smallInteger('submission_progress_temp')->default(1);
});
foreach ($this->getStepMap() as $oldValue => $newValue) {
DB::table('submissions')
->where('submission_progress', $newValue)
->update([
'submission_progress_temp' => $oldValue,
]);
}
Schema::table('submissions', function (Blueprint $table) {
$table->dropColumn('submission_progress');
$table->renameColumn('submission_progress_temp', 'submission_progress');
});
}
/**
* @return array [oldValue => newValue]
*/
protected function getStepMap(): array
{
return [
0 => '',
1 => 'files',
2 => 'contributors',
3 => 'review',
];
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7245_UpdateUserLocaleStringToParsableJsonString.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 I7245_UpdateUserLocaleStringToParsableJsonString
*
* @brief Update the users locales columns where multiple locales are presented as simple string separated by colon(:)
*
* @see https://github.com/pkp/pkp-lib/issues/7245
*/
// en:fr_CA:fr_FR
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I7245_UpdateUserLocaleStringToParsableJsonString extends Migration
{
public function up(): void
{
DB::table('users')
->select(['user_id', 'locales'])
->where('locales', '<>', '[]')
->cursor()
->each(function ($user) {
$parsedLocales = @json_decode($user->locales, true);
if (!is_array($parsedLocales)) {
$locales = explode(':', $user->locales);
if (is_array($locales)) {
DB::table('users')
->where('user_id', $user->user_id)
->update(['locales' => json_encode($locales)]);
}
}
});
}
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7249_UpdateUsersUniqueIndex.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 I7249_UpdateUsersUniqueIndex
*
* @brief Update the users table constraints to reflect case sensitive username, email indexes for postgres DB (For ver. >= 3.3.0)
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I7249_UpdateUsersUniqueIndex extends Migration
{
public function up(): void
{
switch (DB::getDriverName()) {
case 'pgsql':
DB::unprepared('ALTER TABLE users DROP CONSTRAINT users_username;');
DB::unprepared('ALTER TABLE users DROP CONSTRAINT users_email;');
DB::unprepared('CREATE UNIQUE INDEX users_username on users (LOWER(username));');
DB::unprepared('CREATE UNIQUE INDEX users_email on users (LOWER(email));');
break;
}
}
public function down(): void
{
switch (DB::getDriverName()) {
case 'pgsql':
DB::unprepared('DROP INDEX IF EXISTS users_username;');
DB::unprepared('DROP INDEX IF EXISTS users_email;');
Schema::table('users', function ($table) {
$table->unique(['username'], 'users_username');
$table->unique(['email'], 'users_email');
});
break;
}
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7249_UpdateUsersUniqueIndex_v3_1.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 I7249_UpdateUsersUniqueIndex_v3_1
*
* @brief Update the users table constraints to reflect case sensitive username, email indexes for postgres DB (For ver. 3.1.0, 3.2.0, 3.2.1)
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\migration\Migration;
class I7249_UpdateUsersUniqueIndex_v3_1 extends Migration
{
public function up(): void
{
switch (DB::getDriverName()) {
case 'pgsql':
DB::unprepared('DROP INDEX IF EXISTS users_username;');
DB::unprepared('DROP INDEX IF EXISTS users_email;');
DB::unprepared('CREATE UNIQUE INDEX users_username on users (LOWER(username));');
DB::unprepared('CREATE UNIQUE INDEX users_email on users (LOWER(email));');
break;
}
}
public function down(): void
{
switch (DB::getDriverName()) {
case 'pgsql':
DB::unprepared('DROP INDEX IF EXISTS users_username;');
DB::unprepared('DROP INDEX IF EXISTS users_email;');
DB::unprepared('CREATE UNIQUE INDEX users_username on users (username);');
DB::unprepared('CREATE UNIQUE INDEX users_email on users (email);');
break;
}
}
}
@@ -0,0 +1,464 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7264_UpdateEmailTemplates.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 I7264_UpdateEmailTemplates
*
* @brief Describe upgrade/downgrade operations for DB table email_templates.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
abstract class I7264_UpdateEmailTemplates extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Rename email template keys
foreach ([
'email_templates',
'email_templates_default',
'email_templates_default_data'
] as $tableName) {
DB::table($tableName)->where('email_key', 'USER_VALIDATE')
->update(['email_key' => 'USER_VALIDATE_CONTEXT']);
DB::table($tableName)->where('email_key', 'PUBLISH_NOTIFY')
->update(['email_key' => 'ISSUE_PUBLISH_NOTIFY']);
DB::table($tableName)->where('email_key', 'REVIEW_REQUEST_REMIND_AUTO')
->update(['email_key' => 'REVIEW_RESPONSE_OVERDUE_AUTO']);
DB::table($tableName)->where('email_key', 'REVIEW_REQUEST_REMIND_AUTO_ONECLICK')
->update(['email_key' => 'REVIEW_RESPONSE_OVERDUE_AUTO_ONECLICK']);
}
// Add new template for email which is sent to a user registered from a site
DB::table('email_templates_default')->insert([
'email_key' => 'USER_VALIDATE_SITE',
'can_disable' => 0,
]);
DB::table('email_templates_default_data')->insertUsing([
'email_key',
'locale',
'subject',
'body',
'description'
], function (Builder $q) {
$q->selectRaw('? as email_key', ['USER_VALIDATE_SITE'])
->addSelect('locale', 'subject', 'body', 'description')
->from('email_templates_default_data')
->where('email_key', '=', 'USER_VALIDATE_CONTEXT');
});
// Replace all template variables
$oldNewVariablesMap = $this->oldNewVariablesMap();
$this->renameTemplateVariables($oldNewVariablesMap);
}
/**
* Reverse the downgrades
*/
public function down(): void
{
// Revert variables renaming
$newOldVariablesMap = [];
foreach ($this->oldNewVariablesMap() as $emailKey => $variablesMap) {
$newOldVariablesMap[$emailKey] = array_flip($variablesMap);
}
$this->renameTemplateVariables($newOldVariablesMap);
// Revert renaming email template keys
foreach ([
'email_templates',
'email_templates_default',
'email_templates_default_data'
] as $tableName) {
DB::table($tableName)->where('email_key', 'USER_VALIDATE_CONTEXT')
->update(['email_key' => 'USER_VALIDATE']);
DB::table($tableName)->where('email_key', 'ISSUE_PUBLISH_NOTIFY')
->update(['email_key' => 'PUBLISH_NOTIFY']);
DB::table($tableName)->where('email_key', 'REVIEW_RESPONSE_OVERDUE_AUTO')
->update(['email_key' => 'REVIEW_REQUEST_REMIND_AUTO']);
DB::table($tableName)->where('email_key', 'REVIEW_RESPONSE_OVERDUE_AUTO_ONECLICK')
->update(['email_key' => 'REVIEW_REQUEST_REMIND_AUTO_ONECLICK']);
DB::table($tableName)->where('email_key', 'USER_VALIDATE_SITE')->delete();
}
}
/**
* Replaces email template variables in templates' subject and body
*/
protected function renameTemplateVariables(array $oldNewVariablesMap): void
{
foreach ($oldNewVariablesMap as $emailKey => $variablesMap) {
$variables = [];
$replacements = [];
foreach ($variablesMap as $key => $value) {
$variables[] = '/\{\$' . $key . '\}/';
$replacements[] = '{$' . $value . '}';
}
// Default templates
$data = DB::table('email_templates_default_data')->where('email_key', $emailKey)->get();
$data->each(function (object $entry) use ($variables, $replacements) {
$subject = preg_replace($variables, $replacements, $entry->subject);
$body = preg_replace($variables, $replacements, $entry->body);
DB::table('email_templates_default_data')
->where('email_key', $entry->{'email_key'})
->where('locale', $entry->{'locale'})
->update(['subject' => $subject, 'body' => $body]);
});
// Custom templates
$customData = DB::table('email_templates')->where('email_key', $emailKey)->get();
$customData->each(function (object $customEntry) use ($variables, $replacements) {
$emailRows = DB::table('email_templates_settings')->where('email_id', $customEntry->{'email_id'})->get();
foreach ($emailRows as $emailRow) {
$value = preg_replace($variables, $replacements, $emailRow->{'setting_value'});
DB::table('email_templates_settings')
->where('email_id', $emailRow->{'email_id'})
->where('locale', $emailRow->{'locale'})
->where('setting_name', $emailRow->{'setting_name'})
->update(['setting_value' => $value]);
}
});
}
}
/**
* @return array [email_key => [old_variable => new_variable]]
*/
protected function oldNewVariablesMap(): array
{
return [
'NOTIFICATION' => [
'url' => 'notificationUrl',
'principalContactSignature' => 'contextSignature',
'siteTitle' => 'contextName',
],
'NOTIFICATION_CENTER_DEFAULT' => [
'contextName' => 'contextName',
],
'PASSWORD_RESET_CONFIRM' => [
'url' => 'passwordResetUrl',
'principalContactSignature' => 'siteContactName',
],
'USER_REGISTER' => [
'userFullName' => 'recipientName',
'principalContactSignature' => 'contextSignature',
'contextName' => 'contextName',
'username' => 'recipientUsername',
],
// new template from USER_VALIDATE
'USER_VALIDATE_CONTEXT' => [
'userFullName' => 'recipientName',
'principalContactSignature' => 'contextSignature',
'contextName' => 'contextName',
],
// new template from USER_VALIDATE
'USER_VALIDATE_SITE' => [
'userFullName' => 'recipientName',
'principalContactSignature' => 'siteContactName',
'contextName' => 'siteTitle',
],
'REVIEWER_REGISTER' => [
'contextName' => 'contextName',
'principalContactSignature' => 'contextSignature',
'username' => 'recipientUsername'
],
// renamed from PUBLISH_NOTIFY
'ISSUE_PUBLISH_NOTIFY' => [
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'editorialContactSignature' => 'signature',
],
'LOCKSS_EXISTING_ARCHIVE' => [
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'principalContactSignature' => 'signature',
],
'LOCKSS_NEW_ARCHIVE' => [
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'principalContactSignature' => 'signature',
],
'SUBMISSION_ACK' => [
'authorName' => 'recipientName',
'contextName' => 'contextName',
'authorUsername' => 'recipientUsername',
'editorContactSignature' => 'contextSignature',
'submissionUrl' => 'authorSubmissionUrl',
],
'SUBMISSION_ACK_NOT_USER' => [
'contextName' => 'contextName',
'editorialContactSignature' => 'contextSignature',
],
// submissionUrl and editorUsername/recipientUsername are used only in the old template
'EDITOR_ASSIGN' => [
'editorialContactName' => 'recipientName',
'contextName' => 'contextName',
'editorUsername' => 'recipientUsername',
],
'REVIEW_CANCEL' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
],
'REVIEW_REINSTATE' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
],
'REVIEW_REQUEST' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'passwordResetUrl' => 'passwordLostUrl',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
],
'REVIEW_REQUEST_SUBSEQUENT' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'passwordResetUrl' => 'passwordLostUrl',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
],
'REVIEW_REQUEST_ONECLICK' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
],
'REVIEW_REQUEST_ONECLICK_SUBSEQUENT' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
],
'REVIEW_REQUEST_ATTACHED' => [
'reviewerName' => 'recipientName',
'editorialContactSignature' => 'signature',
],
'REVIEW_REQUEST_ATTACHED_SUBSEQUENT' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'editorialContactSignature' => 'signature',
],
// renamed from REVIEW_REQUEST_REMIND_AUTO
'REVIEW_RESPONSE_OVERDUE_AUTO' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'contextSignature',
],
// renamed from REVIEW_REQUEST_REMIND_AUTO_ONECLICK
'REVIEW_RESPONSE_OVERDUE_AUTO_ONECLICK' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'contextSignature',
],
'REVIEW_CONFIRM' => [
'contextName' => 'contextName',
'reviewerName' => 'senderName',
],
'REVIEW_DECLINE' => [
'contextName' => 'contextName',
'reviewerName' => 'senderName',
],
'REVIEW_ACK' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
],
'REVIEW_REMIND' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
'passwordResetUrl' => 'passwordLostUrl',
],
'REVIEW_REMIND_AUTO' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'contextSignature',
],
'REVIEW_REMIND_ONECLICK' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'signature',
],
'REVIEW_REMIND_AUTO_ONECLICK' => [
'reviewerName' => 'recipientName',
'contextName' => 'contextName',
'submissionReviewUrl' => 'reviewAssignmentUrl',
'editorialContactSignature' => 'contextSignature',
],
'EDITOR_DECISION_ACCEPT' => [
'authorName' => 'authors',
'contextName' => 'contextName',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_SEND_TO_EXTERNAL' => [
'authorName' => 'authors',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_SEND_TO_PRODUCTION' => [
'authorName' => 'authors',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_REVISIONS' => [
'authorName' => 'authors',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_RESUBMIT' => [
'authorName' => 'authors',
'contextName' => 'contextName',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_DECLINE' => [
'authorName' => 'authors',
'contextName' => 'contextName',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_DECISION_INITIAL_DECLINE' => [
'authorName' => 'authors',
'submissionUrl' => 'authorSubmissionUrl',
],
'EDITOR_RECOMMENDATION' => [
'contextName' => 'contextName',
],
'COPYEDIT_REQUEST' => [
'participantName' => 'recipientName',
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'participantUsername' => 'recipientUsername',
'contextAcronym' => 'contextAcronym',
],
'LAYOUT_REQUEST' => [
'participantName' => 'recipientName',
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'participantUsername' => 'recipientUsername',
],
'LAYOUT_COMPLETE' => [
'editorialContactName' => 'recipientName',
'contextName' => 'contextName',
'participantName' => 'senderName', // OJS
'signatureFullName' => 'senderName', // OMP
],
'EMAIL_LINK' => [
'authorName' => 'authors',
'contextName' => 'contextName',
'articleUrl' => 'submissionUrl',
'monographUrl' => 'submissionUrl',
],
'SUBSCRIPTION_NOTIFY' => [
'subscriberName' => 'recipientName',
'contextName' => 'contextName',
'username' => 'recipientUsername',
'subscriptionContactSignature' => 'subscriptionSignature',
],
'OPEN_ACCESS_NOTIFY' => [
'contextName' => 'contextName',
'contextUrl' => 'contextUrl',
'editorialContactSignature' => 'contextSignature',
],
'SUBSCRIPTION_BEFORE_EXPIRY' => [
'subscriberName' => 'recipientName',
'contextName' => 'contextName',
'username' => 'recipientUsername',
'subscriptionContactSignature' => 'subscriptionSignature',
],
'SUBSCRIPTION_AFTER_EXPIRY' => [
'subscriberName' => 'recipientName',
'contextName' => 'contextName',
'username' => 'recipientUsername',
'subscriptionContactSignature' => 'subscriptionSignature',
],
'SUBSCRIPTION_AFTER_EXPIRY_LAST' => [
'subscriberName' => 'recipientName',
'contextName' => 'contextName',
'username' => 'recipientUsername',
'subscriptionContactSignature' => 'subscriptionSignature',
],
'SUBSCRIPTION_PURCHASE_INDL' => [
'contextName' => 'contextName',
'userDetails' => 'subscriberDetails',
],
'SUBSCRIPTION_PURCHASE_INSTL' => [
'contextName' => 'contextName',
'userDetails' => 'subscriberDetails',
],
'SUBSCRIPTION_RENEW_INDL' => [
'contextName' => 'contextName',
'userDetails' => 'subscriberDetails',
],
'SUBSCRIPTION_RENEW_INSTL' => [
'contextName' => 'contextName',
'userDetails' => 'subscriberDetails',
],
'CITATION_EDITOR_AUTHOR_QUERY' => [
'authorFirstName' => 'recipientName',
'userFirstName' => 'senderName',
'contextName' => 'contextName',
],
'REVISED_VERSION_NOTIFY' => [
'editorialContactSignature' => 'signature',
'authorName' => 'submitterName',
],
'STATISTICS_REPORT_NOTIFICATION' => [
'principalContactSignature' => 'contextSignature',
'name' => 'recipientName',
],
'ANNOUNCEMENT' => [
'title' => 'announcementTitle',
'summary' => 'announcementSummary',
'url' => 'announcementUrl',
],
'MANUAL_PAYMENT_NOTIFICATION' => [
'itemName' => 'paymentName',
'itemCost' => 'paymentAmount',
'itemCurrencyCode' => 'paymentCurrencyCode',
'userName' => 'senderUsername',
'userFullName' => 'senderName',
],
// in OPS only
'POSTED_ACK' => [
'authorName' => 'recipientName',
'publicationUrl' => 'submissionUrl',
'editorialContactSignature' => 'signature'
],
'INDEX_REQUEST' => [
'participantName' => 'recipientName',
'contextName' => 'contextName',
'participantUsername' => 'recipientUsername',
'editorialContactSignature' => 'signature',
'contextUrl' => 'contextUrl',
],
'INDEX_COMPLETE' => [
'editorialContactName' => 'recipientName',
'contextName' => 'contextName',
'signatureFullName' => 'senderName',
],
];
}
}
@@ -0,0 +1,122 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7265_EditorialDecisions.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 I7265_EditorialDecisions
*
* @brief Database migrations for editorial decision refactor.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
abstract class I7265_EditorialDecisions extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$this->upReviewRounds();
$this->upNotifyAllAuthors();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->downReviewRounds();
$this->downNotifyAllAuthors();
}
/**
* Use null instead of 0 for editorial decisions not in review rounds
*/
protected function upReviewRounds()
{
Schema::table('edit_decisions', function (Blueprint $table) {
$table->bigInteger('review_round_id')->nullable()->change();
$table->bigInteger('round')->nullable()->change();
});
DB::table('edit_decisions')
->where('review_round_id', '=', 0)
->orWhere('round', '=', 0)
->update([
'review_round_id' => null,
'round' => null
]);
Schema::table('edit_decisions', function (Blueprint $table) {
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds');
$table->index(['review_round_id'], 'edit_decisions_review_round_id');
});
}
/**
* Restore 0 values instead of null for editorial decisions not in review rounds
*/
protected function downReviewRounds()
{
Schema::table('edit_decisions', function (Blueprint $table) {
$table->dropForeign(['review_round_id']);
});
DB::table('edit_decisions')
->whereNull('review_round_id')
->orWhereNull('round')
->update([
'review_round_id' => 0,
'round' => 0
]);
Schema::table('edit_decisions', function (Blueprint $table) {
$table->bigInteger('review_round_id')->nullable(false)->change();
$table->bigInteger('round')->nullable(false)->change();
});
}
/**
* Enable the new context setting "notifyAllAuthors"
*/
protected function upNotifyAllAuthors()
{
DB::table($this->getContextSettingsTable())
->insert(
DB::table($this->getContextTable())
->pluck($this->getContextIdColumn())
->map(
function (int $contextId) {
return [
$this->getContextIdColumn() => $contextId,
'setting_name' => 'notifyAllAuthors',
'setting_value' => 1,
];
}
)->toArray()
);
}
/**
* Delete the new context setting "notifyAllAuthors"
*/
protected function downNotifyAllAuthors()
{
DB::table($this->getContextSettingsTable())
->where('setting_name', 'notifyAllAuthors')
->delete();
}
abstract protected function getContextTable(): string;
abstract protected function getContextSettingsTable(): string;
abstract protected function getContextIdColumn(): string;
}
@@ -0,0 +1,49 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7286_BatchesMigration.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 I7286_BatchesMigration
*
* @brief Database migrations for batches refactor, see https://github.com/illuminate/queue/blob/9.x/Console/stubs/batches.stub
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I7286_BatchesMigration extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->text('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
}
/**
* Revert the migrations
*/
public function down(): void
{
Schema::dropIfExists('job_batches');
}
}
@@ -0,0 +1,79 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7287_RemoveEmailTemplatesDefault.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 I7287_RemoveEmailTemplatesDefault
*
* @brief Remove default email template table after data migrated to the Mailable class
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I7287_RemoveEmailTemplatesDefault extends Migration
{
protected Collection $emailTemplatesDefault;
protected Collection $emailTemplateDefaultData;
/**
* Run the migrations.
*/
public function up(): void
{
$this->emailTemplatesDefault = DB::table('email_templates_default')->get();
$this->emailTemplateDefaultData = DB::table('email_templates_default_data')->get();
Schema::drop('email_templates_default');
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->dropColumn('description');
});
}
/**
* Revert the migrations
*/
public function down(): void
{
// Recreate email_templates_default table
Schema::create('email_templates_default', function (Blueprint $table) {
$table->bigInteger('email_id')->autoIncrement();
$table->string('email_key', 255)->comment('Unique identifier for this email.');
$table->smallInteger('can_disable')->default(0);
$table->smallInteger('can_edit')->default(0);
$table->bigInteger('from_role_id')->nullable();
$table->bigInteger('to_role_id')->nullable();
$table->bigInteger('stage_id')->nullable();
$table->index(['email_key'], 'email_templates_default_email_key');
});
$this->emailTemplatesDefault->each(function (\stdClass $row) {
DB::table('email_templates_default')->insert([
'email_key' => $row->{'email_key'},
'can_disable' => $row->{'can_disable'},
'can_edit' => $row->{'can_edit'},
'from_role_id' => $row->{'from_role_id'},
'to_role_id' => $row->{'to_role_id'},
]);
});
// Re-add description column to the email_templates_default_data table and populate with data
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->addColumn('text', 'description')->nullable();
});
$this->emailTemplateDefaultData->each(function (\stdClass $dataRow) {
DB::table('email_templates_default_data')
->where('email_key', $dataRow->{'email_key'})
->where('locale', $dataRow->locale)
->update(['description' => $dataRow->description]);
});
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7366_UpdateUserAPIKeySettings.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 I7366_UpdateUserAPIKeySettings
*
* @brief Describe upgrade/downgrade for updating user API related settings
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
class I7366_UpdateUserAPIKeySettings extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$users = DB::select(
DB::raw(
"SELECT u.user_id FROM users u
JOIN user_settings enabled_setting ON (enabled_setting.user_id = u.user_id AND enabled_setting.setting_name = 'apiKeyEnabled')
LEFT JOIN user_settings key_setting ON (key_setting.user_id = u.user_id AND key_setting.setting_name = 'apiKey')
WHERE key_setting.user_id IS NULL"
)
);
collect($users)
->pluck('user_id')
->chunk(1000)
->each(
fn ($ids) => DB::table('user_settings')
->where('setting_name', 'apiKeyEnabled')
->whereIn('user_id', $ids->toArray())
->delete()
);
}
/**
* Reverse the migration.
*/
public function down(): void
{
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7463_LocaleColumn.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 I7463_LocaleColumn
*
* @brief Remove's locale column installed to publications table
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class I7463_LocaleColumn extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasColumn('publications', 'locale')) {
Schema::table('publications', function (Blueprint $table) {
$table->dropColumn('locale');
});
}
}
/**
* Reverse the downgrades
*/
public function down(): void
{
// Don't restore this column on downgrade
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7474_UpdateMimetypes.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 I7474_UpdateMimetypes
*
* @brief Updates the mimetype of some files that were not detected properly
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
class I7474_UpdateMimetypes extends \PKP\migration\Migration
{
/**
* Run the migrations
*/
public function up(): void
{
DB::table('files')
->where('mimetype', '=', 'video/x-ms-asf')
->where('path', 'like', '%.wma')
->update(['mimetype' => 'video/x-ms-wma']);
DB::table('files')
->where('mimetype', '=', 'video/x-ms-asf')
->where('path', 'like', '%.wmv')
->update(['mimetype' => 'video/x-ms-wmv']);
DB::table('files')
->where('mimetype', '=', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
->where('path', 'like', '%.docm')
->update(['mimetype' => 'application/vnd.ms-word.document.macroEnabled.12']);
DB::table('files')
->where('mimetype', '=', 'text/plain')
->where('path', 'like', '%.csv')
->update(['mimetype' => 'text/csv']);
}
/**
* Reverse the migrations
*/
public function down(): void
{
// Don't restore the old mimetypes on downgrade
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7486_RemoveItemViewsTable.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 I7486_RemoveItemViewsTable
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I7486_RemoveItemViewsTable extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::drop('item_views');
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::create('item_views', function (Blueprint $table) {
$table->bigInteger('assoc_type');
$table->bigInteger('assoc_id');
$table->bigInteger('user_id')->nullable();
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'item_views_user_id');
$table->datetime('date_last_viewed')->nullable();
$table->unique(['assoc_type', 'assoc_id', 'user_id'], 'item_views_pkey');
});
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7486_RenameUnconsideredColumnToConsidered.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 I7486_RenameUnconsideredColumnToConsidered
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I7486_RenameUnconsideredColumnToConsidered extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('review_assignments', function (Blueprint $table) {
$table->renameColumn('unconsidered', 'considered');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('review_assignments', function (Blueprint $table) {
$table->renameColumn('considered', 'unconsidered');
});
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7592_RemoveUnusedEmailTemplates.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 I7592_RemoveUnusedEmailTemplates
*
* @brief Describe upgrade/downgrade for removing unused reviewer related email templates from the default data
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use stdClass;
class I7592_RemoveUnusedEmailTemplates extends \PKP\migration\Migration
{
protected ?Collection $templatesDefaultData;
protected ?Collection $templatesDefault;
public function up(): void
{
$emailKeys = [
'REVIEW_REMIND_AUTO_ONECLICK',
'REVIEW_RESPONSE_OVERDUE_AUTO_ONECLICK',
'REVIEW_REMIND_ONECLICK',
'REVIEW_REQUEST_ATTACHED',
'REVIEW_REQUEST_ATTACHED_SUBSEQUENT',
'REVIEW_REQUEST_ONECLICK',
'REVIEW_REQUEST_ONECLICK_SUBSEQUENT',
'CITATION_EDITOR_AUTHOR_QUERY',
];
$this->templatesDefault = DB::table('email_templates_default')
->whereIn('email_key', $emailKeys)
->get();
$this->templatesDefaultData = DB::table('email_templates_default_data')
->whereIn('email_key', $emailKeys)
->get();
DB::table('email_templates_default')
->whereIn('email_key', $emailKeys)
->delete();
DB::table('email_templates_default_data')
->whereIn('email_key', $emailKeys)
->delete();
}
public function down(): void
{
$this->templatesDefault->each(function (stdClass $templateDefaultRow) {
DB::table('email_templates_default')->insert((array) $templateDefaultRow);
});
$this->templatesDefaultData->each(function (stdClass $templateDefaultDataRow) {
DB::table('email_templates_default_data')->insert((array) $templateDefaultDataRow);
});
}
}
@@ -0,0 +1,90 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7624_StrftimeDeprecation.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 I7624_StrftimeDeprecation
*
* @brief Convert strftime-based date formats into DateTime::format instead.
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Application;
use Illuminate\Support\Facades\DB;
use PKP\core\PKPString;
class I7624_StrftimeDeprecation extends \PKP\migration\Migration
{
private const DATETIME_SETTINGS = [
'dateFormatShort',
'dateFormatLong',
'timeFormat',
'datetimeFormatShort',
'datetimeFormatLong',
];
private const CONTEXT_SETTING_TABLE_NAMES = [
'ojs2' => 'journal_settings',
'omp' => 'press_settings',
'ops' => 'server_settings',
];
private const CONTEXT_SETTING_TABLE_KEYS = [
'ojs2' => 'journal_id',
'omp' => 'press_id',
'ops' => 'server_id',
];
/**
* Run the migrations.
*/
public function up(): void
{
$this->convert('up');
}
/**
* Reverse the downgrades
*/
public function down(): void
{
$this->convert('down');
}
/**
* Convert the date format settings.
*
* @param string Direction 'up'|'down'
*/
private function convert(string $direction)
{
$applicationName = Application::get()->getName();
$contextIdColumnName = self::CONTEXT_SETTING_TABLE_KEYS[$applicationName];
$dateSettingValues = DB::table(self::CONTEXT_SETTING_TABLE_NAMES[$applicationName])
->whereIn('setting_name', self::DATETIME_SETTINGS)
->select(['setting_value', $contextIdColumnName, 'setting_name', 'locale'])
->get();
$map = PKPString::getStrftimeConversion();
switch ($direction) {
case 'up': break;
case 'down': $map = array_flip($map);
break;
default: throw new \Exception("Unknown direction {$direction}");
}
foreach ($dateSettingValues as $row) {
DB::table(self::CONTEXT_SETTING_TABLE_NAMES[$applicationName])
->where('setting_name', '=', $row->setting_name)
->where('locale', '=', $row->locale)
->where($contextIdColumnName, '=', $row->$contextIdColumnName)
->update(
[
'setting_value' => strtr($row->setting_value, $map)
]
);
}
}
}
@@ -0,0 +1,133 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7725_DecisionConstantsUpdate.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 I7725_DecisionConstantsUpdate
*
* @brief Editorial decision constant sync up across all application
*
* @see https://github.com/pkp/pkp-lib/issues/7725
*/
namespace PKP\migration\upgrade\v3_4_0;
use Carbon\Carbon;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
use Throwable;
abstract class I7725_DecisionConstantsUpdate extends Migration
{
/**
* Get the decisions constants mappings
*
*/
abstract public function getDecisionMappings(): array;
/**
* Run the migrations.
*/
public function up(): void
{
$this->configureUpdatedAtColumn();
try {
DB::beginTransaction();
collect($this->getDecisionMappings())
->each(
fn ($decisionMapping) => DB::table('edit_decisions')
->when(
isset($decisionMapping['stage_id']) && !empty($decisionMapping['stage_id']),
fn ($query) => $query->whereIn('stage_id', $decisionMapping['stage_id'])
)
->where('decision', $decisionMapping['current_value'])
->whereNull('updated_at')
->update([
'decision' => $decisionMapping['updated_value'],
'updated_at' => Carbon::now(),
])
);
DB::commit();
} catch (Throwable $exception) {
DB::rollBack();
$this->_installer->log($exception->__toString());
}
$this->removeUpdatedAtColumn();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->configureUpdatedAtColumn();
try {
DB::beginTransaction();
collect($this->getDecisionMappings())
->each(
fn ($decisionMapping) => DB::table('edit_decisions')
->when(
isset($decisionMapping['stage_id']) && !empty($decisionMapping['stage_id']),
fn ($query) => $query->whereIn('stage_id', $decisionMapping['stage_id'])
)
->where('decision', $decisionMapping['updated_value'])
->whereNull('updated_at')
->update([
'decision' => $decisionMapping['current_value'],
'updated_at' => Carbon::now(),
])
);
DB::commit();
} catch (Throwable $exception) {
DB::rollBack();
$this->_installer->log($exception->__toString());
}
$this->removeUpdatedAtColumn();
}
/**
* Set the temporary updated_at column with NULL value
*/
protected function configureUpdatedAtColumn(): void
{
if (!Schema::hasColumn('edit_decisions', 'updated_at')) {
Schema::table('edit_decisions', function (Blueprint $table) {
$table->timestamp('updated_at')->nullable();
});
return;
}
DB::table('edit_decisions')->update(['updated_at' => null]);
return;
}
/**
* Drop the temporary updated_at column
*/
protected function removeUpdatedAtColumn(): void
{
if (Schema::hasColumn('edit_decisions', 'updated_at')) {
Schema::table('edit_decisions', function (Blueprint $table) {
$table->dropColumn('updated_at');
});
}
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I7874_NotificationMetadataModifiedRemove.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 I7874_NotificationMetadataModifiedRemove
*
* @brief Removes deprecated PKPNotification::NOTIFICATION_TYPE_METADATA_MODIFIED setting
*
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use PKP\migration\Migration;
use stdClass;
class I7874_NotificationMetadataModifiedRemove extends Migration
{
protected Collection $subscribedToMetadataChangedNotification;
/**
* Run the migrations.
*/
public function up(): void
{
$this->subscribedToMetadataChangedNotification = DB::table('notification_subscription_settings')
->where('setting_value', '=', 0x1000002) // PKP\notification\PKPNotification::NOTIFICATION_TYPE_METADATA_MODIFIED
->get();
$this->subscribedToMetadataChangedNotification->each(function (stdClass $row) {
DB::table('notification_subscription_settings')
->where('setting_id', '=', $row->{'setting_id'})
->delete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$this->subscribedToMetadataChangedNotification->each(function (stdClass $row) {
$values = (array) $row;
unset($values['setting_id']);
DB::table('notification_subscription_settings')->insert([$values]);
});
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8060_UpdateUserLocalesDefaultToEmptyArrayFromNull.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 I8060_UpdateUserLocalesDefaultToEmptyArray
*
* @brief Update the users table locales column default to empty array from NULL and update existing NULL ones to []
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I8060_UpdateUserLocalesDefaultToEmptyArrayFromNull extends Migration
{
public function up(): void
{
DB::table('users')->whereNull('locales')->update(['locales' => '[]']);
Schema::table('users', function ($table) {
$table->string('locales', 255)->nullable(false)->default('[]')->change();
});
}
public function down(): void
{
Schema::table('users', function ($table) {
$table->string('locales', 255)->nullable()->default('[]')->change();
});
}
}
@@ -0,0 +1,212 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8073_RemoveNotesWithoutQueriesAndRelatedObjects.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 I8073_RemoveNotesWithoutQueriesAndRelatedObjects
*
* @brief Removes Notes without Queries and related objects
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use PKP\config\Config;
use PKP\migration\Migration;
class I8073_RemoveNotesWithoutQueriesAndRelatedObjects extends Migration
{
private const ASSOC_TYPE_NOTE = 0x0000208; // PKPApplication::ASSOC_TYPE_NOTE
private const ASSOC_TYPE_QUERY = 0x010000a; // PKPApplication::ASSOC_TYPE_QUERY
private const FILE_MODE_MASK = 0666; // FileManager::FILE_MODE_MASK
private const DIRECTORY_MODE_MASK = 0777; // FileManager::DIRECTORY_MODE_MASK
public function up(): void
{
// Create a Filesystem object with the appropriate adapter to access the actual files
$umask = Config::getVar('files', 'umask', 0022);
$adapter = new LocalFilesystemAdapter(
Config::getVar('files', 'files_dir'),
PortableVisibilityConverter::fromArray([
'file' => [
'public' => self::FILE_MODE_MASK & ~$umask,
'private' => self::FILE_MODE_MASK & ~$umask,
],
'dir' => [
'public' => self::DIRECTORY_MODE_MASK & ~$umask,
'private' => self::DIRECTORY_MODE_MASK & ~$umask,
]
]),
LOCK_EX,
LocalFilesystemAdapter::DISALLOW_LINKS
);
$filesystem = new Filesystem($adapter);
// Does not have the foreign key reference
Schema::table('notification_settings', function (Blueprint $table) {
$table->foreign('notification_id')->references('notification_id')->on('notifications')->onDelete('cascade');
});
// Does have the foreign key reference but not the CASCADE
if (DB::getDoctrineSchemaManager()->introspectTable('submission_files')->hasForeignKey('submission_files_file_id_foreign')) {
Schema::table('submission_files', fn (Blueprint $table) => $table->dropForeign('submission_files_file_id_foreign'));
}
Schema::table('submission_files', function (Blueprint $table) {
$table->foreign('file_id')->references('file_id')->on('files')->onDelete('cascade');
});
// Does have the foreign key reference but not the CASCADE
foreach (['submission_file_revisions_submission_file_id_foreign', 'submission_file_revisions_file_id_foreign'] as $foreignKeyName) {
if (DB::getDoctrineSchemaManager()->introspectTable('submission_file_revisions')->hasForeignKey($foreignKeyName)) {
Schema::table('submission_file_revisions', fn (Blueprint $table) => $table->dropForeign($foreignKeyName));
}
}
Schema::table('submission_file_revisions', function (Blueprint $table) {
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->foreign('file_id')->references('file_id')->on('files')->onDelete('cascade');
});
// Does not have the foreign key reference
Schema::table('submission_file_settings', function (Blueprint $table) {
$table->foreignId('submission_file_id')->change();
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
});
// Does have the foreign key reference but not the CASCADE
if (DB::getDoctrineSchemaManager()->introspectTable('review_files')->hasForeignKey('review_files_submission_file_id_foreign')) {
Schema::table('review_files', fn (Blueprint $table) => $table->dropForeign('review_files_submission_file_id_foreign'));
}
Schema::table('review_files', function (Blueprint $table) {
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
$table->index(['submission_file_id'], 'review_files_submission_file_id');
});
// Does have the foreign key reference but not the CASCADE
if (DB::getDoctrineSchemaManager()->introspectTable('review_round_files')->hasForeignKey('review_round_files_submission_file_id_foreign')) {
Schema::table('review_round_files', fn (Blueprint $table) => $table->dropForeign('review_round_files_submission_file_id_foreign'));
}
Schema::table('review_round_files', function (Blueprint $table) {
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files')->onDelete('cascade');
});
// Does not have the foreign key reference
Schema::table('query_participants', function (Blueprint $table) {
$table->foreign('query_id')->references('query_id')->on('queries')->onDelete('cascade');
$table->index(['query_id'], 'query_participants_query_id');
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'query_participants_user_id');
});
$orphanedIds = DB::table('notes AS n')
->leftJoin('queries AS q', 'n.assoc_id', '=', 'q.query_id')
->where('n.assoc_type', '=', self::ASSOC_TYPE_QUERY)
->whereNull('q.query_id')
->pluck('n.note_id', 'n.assoc_id');
foreach ($orphanedIds as $neQueryId => $noteId) {
$notesFileRows = DB::table('submission_files as sf')
->join('files as f', 'sf.file_id', '=', 'f.file_id')
->where('sf.assoc_type', '=', self::ASSOC_TYPE_NOTE)
->where('sf.assoc_id', '=', $noteId)
->get([
'sf.submission_file_id as submissionFileId',
'sf.file_id as fileId',
'f.path as filePath'
]);
$filesToCheckForDeletion = [];
foreach ($notesFileRows as $submissionFileRow) {
$submissionFileId = $submissionFileRow->submissionFileId;
$submissionFileFileId = $submissionFileRow->fileId;
$submissionFilePath = $submissionFileRow->filePath;
DB::table('submission_files')
->where('submission_file_id', '=', $submissionFileId)
->delete();
if (!array_key_exists($submissionFileFileId, $filesToCheckForDeletion)) {
$filesToCheckForDeletion[$submissionFileFileId] = $submissionFilePath;
}
}
foreach ($filesToCheckForDeletion as $submissionFileFileId => $submissionFilePath) {
$remainingSubmissionFilesCount = DB::table('submission_files')
->where('file_id', '=', $submissionFileFileId)
->count();
// If the file is not used by another SubmissionFile, it can be deleted.
if ($remainingSubmissionFilesCount == 0) {
if ($filesystem->has($submissionFilePath)) {
try {
$filesystem->delete($submissionFilePath);
$this->_installer->log("A submission file that was attached to an orphaned note with ID {$noteId} at {$submissionFilePath} was successfully deleted.");
} catch (FilesystemException | UnableToDeleteFile $exception) {
$exceptionMessage = $exception->getMessage();
$this->_installer->log("A submission file that was attached to an orphaned note with ID {$noteId} was found at {$submissionFilePath} but could not be deleted because of: {$exceptionMessage}.");
}
}
DB::table('files')
->where('file_id', '=', $submissionFileFileId)
->delete();
}
}
$this->_installer->log("Removing orphaned note entry ID {$noteId} with nonexistent query {$neQueryId}");
DB::table('notes')
->where('note_id', '=', $noteId)
->delete();
}
}
public function down(): void
{
Schema::table('notification_settings', function (Blueprint $table) {
$table->dropForeign('notification_settings_notification_id_foreign');
});
Schema::table('submission_files', function (Blueprint $table) {
$table->dropForeign('submission_files_file_id_foreign');
$table->foreign('file_id')->references('file_id')->on('files');
});
Schema::table('submission_file_revisions', function (Blueprint $table) {
$table->dropForeign('submission_file_revisions_submission_file_id_foreign');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
$table->dropForeign('submission_file_revisions_file_id_foreign');
$table->foreign('file_id')->references('file_id')->on('files');
});
Schema::table('submission_file_settings', function (Blueprint $table) {
$table->bigInteger('submission_file_id')->nullable(false)->unsigned()->change();
$table->dropForeign('submission_file_settings_submission_file_id_foreign');
});
Schema::table('review_files', function (Blueprint $table) {
$table->dropForeign('review_files_submission_file_id_foreign');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
});
Schema::table('review_round_files', function (Blueprint $table) {
$table->dropForeign('review_round_files_submission_file_id_foreign');
$table->foreign('submission_file_id')->references('submission_file_id')->on('submission_files');
});
Schema::table('query_participants', function (Blueprint $table) {
$table->dropForeign('query_participants_query_id_foreign');
$table->dropForeign('query_participants_user_id_foreign');
});
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8093_UpdateUserGroupRelationTablesFK.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 I8093_UpdateUserGroupRelationTablesFK
*
* @brief Update the foreign keys for UserGroup relation tables
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I8093_UpdateUserGroupRelationTablesFK extends Migration
{
public function up(): void
{
Schema::table('user_group_settings', function (Blueprint $table) {
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
});
Schema::table('user_user_groups', function (Blueprint $table) {
$table->foreign('user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
});
Schema::table('user_group_stage', function (Blueprint $table) {
$table->foreign('user_group_id', 'user_group_stage_user_group_id')->references('user_group_id')->on('user_groups')->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('user_group_settings', function (Blueprint $table) {
$table->dropForeign('user_group_settings_user_group_id_foreign');
});
Schema::table('user_user_groups', function (Blueprint $table) {
$table->dropForeign('user_user_groups_user_group_id_foreign');
});
Schema::table('user_group_stage', function (Blueprint $table) {
$table->dropForeign('user_group_stage_user_group_id_foreign');
});
}
}
@@ -0,0 +1,129 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8151_ExtendSettingValues.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 I8151_ExtendSettingValues
*
* @brief Describe upgrade/downgrade operations for extending TEXT columns to MEDIUMTEXT
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class I8151_ExtendSettingValues extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('announcement_type_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('announcement_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('category_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('site_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('user_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('notification_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('notification_subscription_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('email_templates_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('plugin_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('controlled_vocab_entry_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('genre_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('library_file_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('event_log_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('citation_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('filter_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('navigation_menu_item_assignment_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('review_form_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('review_form_element_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('user_group_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('submission_file_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('publication_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('author_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
Schema::table('data_object_tombstone_settings', function (Blueprint $table) {
$table->mediumText('setting_value')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// This downgrade is intentionally not implemented. Changing MEDIUMTEXT back to TEXT
// may result in data truncation. Having MEDIUMTEXT in place of TEXT in an otherwise
// downgraded database will not have side-effects.
}
}
@@ -0,0 +1,125 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8508_ConvertCurrentLogFile.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 I8508_ConvertCurrentLogFile
*
* @brief Convert current and eventually the usage stats log file from yesterday into the new format.
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\statistics\StatisticsHelper;
use Illuminate\Support\Facades\DB;
use PKP\cliTool\traits\ConvertLogFile;
use PKP\config\Config;
use PKP\file\FileManager;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
use PKP\task\UpdateIPGeoDB;
class I8508_ConvertCurrentLogFile extends Migration
{
/**
* Run the migration.
*/
public function up(): void
{
$fileManager = new FileManager();
$convertCurrentUsageStatsLogFile = new ConvertCurrentUsageStatsLogFile();
// If Geo usage stats are enabled download the GeoIPDB
$siteGeoUsageStatsSettings = DB::table('site_settings')
->where('setting_name', '=', 'enableGeoUsageStats')
->value('setting_value');
if ($siteGeoUsageStatsSettings != null && $siteGeoUsageStatsSettings !== 'disabled') {
$geoIPDBFile = StatisticsHelper::getGeoDBPath();
if (!file_exists($geoIPDBFile)) {
$geoIPDB = new UpdateIPGeoDB();
$geoIPDB->execute();
}
}
$counterR5StartDate = date('Y-m-d');
$todayFileName = 'usage_events_' . date('Ymd') . '.log';
if (file_exists($convertCurrentUsageStatsLogFile->getLogFileDir() . '/' . $todayFileName)) {
$convertCurrentUsageStatsLogFile->convert($todayFileName);
$oldFilePath = $convertCurrentUsageStatsLogFile->getLogFileDir() . '/usage_events_' . date('Ymd') . '_old.log';
$oldFileRemoved = $fileManager->deleteByPath($oldFilePath);
if ($oldFileRemoved) {
$this->_installer->log("The old usage stats log file {$oldFilePath} was successfully deleted.");
} else {
$this->_installer->log("The old usage stats log file {$oldFilePath} could not be deleted.");
}
}
$yesterdayFileName = 'usage_events_' . date('Ymd', strtotime('-1 days')) . '.log';
if (file_exists($convertCurrentUsageStatsLogFile->getLogFileDir() . '/' . $yesterdayFileName)) {
$convertCurrentUsageStatsLogFile->convert($yesterdayFileName);
$counterR5StartDate = date('Y-m-d', strtotime('-1 days'));
$oldFilePath = $convertCurrentUsageStatsLogFile->getLogFileDir() . '/usage_events_' . date('Ymd', strtotime('-1 days')) . '_old.log';
$oldFileRemoved = $fileManager->deleteByPath($oldFilePath);
if ($oldFileRemoved) {
$this->_installer->log("The old usage stats log file {$oldFilePath} was successfully deleted.");
} else {
$this->_installer->log("The old usage stats log file {$oldFilePath} could not be deleted.");
}
}
DB::table('site_settings')->insert(['setting_name' => 'counterR5StartDate', 'setting_value' => $counterR5StartDate]);
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
class ConvertCurrentUsageStatsLogFile
{
use ConvertLogFile;
/**
* Constructor.
*/
public function __construct()
{
$this->__constructTrait();
}
public function getLogFileDir(): string
{
return StatisticsHelper::getUsageStatsDirPath() . '/usageEventLogs';
}
public function getParseRegex(): string
{
return '/^(?P<ip>\S+) \S+ \S+ "(?P<date>.*?)" (?P<url>\S+) (?P<returnCode>\S+) "(?P<userAgent>.*?)"/';
}
public function getPhpDateTimeFormat(): string
{
return 'Y-m-d H:i:s';
}
public function isPathInfoDisabled(): bool
{
return Config::getVar('general', 'disable_path_info') ? true : false;
}
public function isApacheAccessLogFile(): bool
{
return false;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8592_SiteNotificationSubscriptions.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 I8592_SiteNotificationSubscriptions
*
* @brief Allow notification subscriptions to have no context for site-wide subscriptions
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class I8592_SiteNotificationSubscriptions extends \PKP\migration\Migration
{
public function up(): void
{
Schema::table('notification_subscription_settings', function (Blueprint $table) {
$table->bigInteger('context')->nullable()->change();
});
}
public function down(): void
{
Schema::table('notification_subscription_settings', function (Blueprint $table) {
$table->bigInteger('context')->nullable(false)->change();
});
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8737_SectionEditorsUniqueIndexUpdate.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 I8737_SectionEditorsUniqueIndexUpdate
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I8737_SectionEditorsUniqueIndexUpdate extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->dropUnique('section_editors_unique');
$table->unique(['context_id', 'assoc_id', 'assoc_type', 'user_id', 'user_group_id'], 'section_editors_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('subeditor_submission_group', function (Blueprint $table) {
$table->dropUnique('section_editors_unique');
$table->unique(['context_id', 'assoc_id', 'assoc_type', 'user_id'], 'section_editors_unique');
});
}
}
@@ -0,0 +1,104 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8866_DispatchRegionCodesFixingJobs.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 I8866_DispatchRegionCodesFixingJobs
*
* @brief Dispatches the jobs (a job per country) that shell fix the old region codes, if needed i.e. if any region exists.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Bus\Batch;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\core\Core;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
use PKP\migration\upgrade\v3_4_0\jobs\FixRegionCodes;
class I8866_DispatchRegionCodesFixingJobs extends Migration
{
/**
* Run the migration.
*/
public function up(): void
{
if (DB::table('metrics_submission_geo_monthly')->whereNotNull('region')->exists() ||
DB::table('metrics_submission_geo_daily')->whereNotNull('region')->exists()) {
// create a temporary table for the FIPS-ISO mapping
if (!Schema::hasTable('region_mapping_tmp')) {
Schema::create('region_mapping_tmp', function (Blueprint $table) {
$table->string('country', 2);
$table->string('fips', 3);
$table->string('iso', 3)->nullable();
});
}
// temporary create index on the column country and region, in order to be able to update the region codes in a reasonable time
Schema::table('metrics_submission_geo_daily', function (Blueprint $table) {
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes('metrics_submission_geo_daily');
if (!array_key_exists('metrics_submission_geo_daily_tmp_index', $indexesFound)) {
$table->index(['country', 'region'], 'metrics_submission_geo_daily_tmp_index');
}
});
Schema::table('metrics_submission_geo_monthly', function (Blueprint $table) {
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes('metrics_submission_geo_monthly');
if (!array_key_exists('metrics_submission_geo_monthly_tmp_index', $indexesFound)) {
$table->index(['country', 'region'], 'metrics_submission_geo_monthly_tmp_index');
}
});
// read the FIPS to ISO mappings and displatch a job per country
$mappings = include Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/regionMapping.php';
$jobs = [];
foreach (array_keys($mappings) as $country) {
$jobs[] = new FixRegionCodes($country);
}
Bus::batch($jobs)
->then(function (Batch $batch) {
// drop the temporary index
Schema::table('metrics_submission_geo_daily', function (Blueprint $table) {
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes('metrics_submission_geo_daily');
if (array_key_exists('metrics_submission_geo_daily_tmp_index', $indexesFound)) {
$table->dropIndex(['tmp']);
}
});
Schema::table('metrics_submission_geo_monthly', function (Blueprint $table) {
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $sm->listTableIndexes('metrics_submission_geo_monthly');
if (array_key_exists('metrics_submission_geo_monthly_tmp_index', $indexesFound)) {
$table->dropIndex(['tmp']);
}
});
// drop the temporary table
if (Schema::hasTable('region_mapping_tmp')) {
Schema::drop('region_mapping_tmp');
}
})
->dispatch();
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,258 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8933_EventLogLocalized.php
*
* Copyright (c) 2023 Simon Fraser University
* Copyright (c) 2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I8933_EventLogLocalized.php
*
* @brief Adds a column to the event_log_settings table to store localized data such as a file name and drops setting_type column.
* In the event_log table allows null values for userId.
* Fixes the issue with duplicate event types and renames conflicting setting names
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class I8933_EventLogLocalized extends Migration
{
public const CHUNK_SIZE = 10000;
abstract protected function getContextTable(): string;
abstract protected function getContextIdColumn(): string;
/**
* Run the migration.
*/
public function up(): void
{
Schema::table('event_log_settings', function (Blueprint $table) {
$table->string('locale', 14)->default('')->after('log_id');
$table->dropUnique('event_log_settings_unique');
$table->unique(['log_id', 'locale', 'setting_name'], 'event_log_settings_unique');
$table->dropColumn('setting_type');
});
// Events can be triggered without a user, e.g., in schedule tasks
Schema::table('event_log', function (Blueprint $table) {
$table->dropForeign('event_log_user_id_foreign');
$table->dropIndex('event_log_user_id');
$table->bigInteger('user_id')->nullable()->change();
$table->foreign('user_id')->references('user_id')->on('users')->onDelete('cascade');
$table->index(['user_id'], 'event_log_user_id');
if (DB::connection() instanceof PostgresConnection) {
DB::unprepared('ALTER TABLE event_log ALTER is_translated TYPE bool USING CASE WHEN COALESCE(is_translated, 0) = 0 THEN FALSE ELSE TRUE END');
} else {
$table->boolean('is_translated')->nullable()->change();
}
});
$this->fixConflictingSubmissionLogConstants();
// Rename ambiguous settings
$this->renameSettings();
// Localize existing submission file name entries
$sitePrimaryLocale = DB::table('site')->value('primary_locale');
$contexts = DB::table($this->getContextTable())->get([$this->getContextIdColumn(), 'primary_locale']);
$contextIdPrimaryLocaleMap = [];
foreach ($contexts as $context) {
$contextIdPrimaryLocaleMap[$context->{$this->getContextIdColumn()}] = $context->primary_locale;
}
DB::table('event_log_settings AS es')
->join('event_log AS e', 'es.log_id', '=', 'e.log_id')
->where('es.setting_name', 'filename')
->whereIn('e.event_type', [
0x50000001, // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_UPLOAD,
0x50000010, // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_EDIT,
0x50000008, // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD,
])
->orderBy('event_log_setting_id')
->chunk(self::CHUNK_SIZE, function (Collection $logChunks) use ($sitePrimaryLocale, $contextIdPrimaryLocaleMap) {
$mapLocaleWithSettingIds = [];
foreach ($logChunks as $row) {
// Get locale based on a submission file ID log entry
$locale = $this->getContextPrimaryLocale($row, $sitePrimaryLocale, $contextIdPrimaryLocaleMap);
if (!$locale) {
continue;
}
$mapLocaleWithSettingIds[$locale] ??= [];
$mapLocaleWithSettingIds[$locale][] = $row->event_log_setting_id;
}
// Update by chunks for each locale
foreach ($mapLocaleWithSettingIds as $locale => $settingIds) {
DB::table('event_log_settings')
->whereIn('event_log_setting_id', $settingIds)
->update(['locale' => $locale]);
}
});
}
/**
* FIX event types with identical values
* 0x40000020: SUBMISSION_LOG_DECISION_EMAIL_SENT => 0x30000007, SUBMISSION_LOG_REVIEW_REMIND => 0x40000020, SUBMISSION_LOG_REVIEW_REMIND_AUTO => 0x40000021
*/
protected function fixConflictingSubmissionLogConstants(): void
{
DB::table('event_log')->where('event_type', 0x40000020)->lazyById(1000, 'log_id')->each(function (object $row) {
if (
DB::table('event_log_settings')
->where('log_id', $row->log_id)
->whereIn('setting_name', ['recipientCount', 'subject'])
->count() === 2
) {
DB::table('event_log')
->where('log_id', $row->log_id)
->update(['event_type' => 0x30000007]); // SUBMISSION_LOG_DECISION_EMAIL_SENT
} elseif (
!DB::table('event_log_settings')
->where('log_id', $row->log_id)
->whereIn('setting_name', ['senderId', 'senderName'])
->exists()
) {
DB::table('event_log')
->where('log_id', $row->log_id)
->update(['event_type' => 0x40000021]); // SUBMISSION_LOG_REVIEW_REMIND_AUTO
}
});
}
/**
* Retrieve the primary locale of the context associated with a given submission file
*/
protected function getContextPrimaryLocale(object $row, string $sitePrimaryLocale, array $contextIdPrimaryLocaleMap): ?string
{
// Try to determine submission/submission file ID based on the assoc type
if ($row->assoc_type == 0x0000203) { // ASSOC_TYPE_SUBMISSION_FILE
$submissionFileId = $row->assoc_id;
} elseif ($row->assoc_type == 0x0100009) { // ASSOC_TYPE_SUBMISSION
$submissionId = $row->assoc_id;
} else {
throw new \Exception('Unsupported assoc_type in the event log: ' . $row->assoc_type);
}
// Get submission from the file ID
if (!isset($submissionId)) {
$submissionId = DB::table('submission_files')
->where('submission_file_id', $submissionFileId)
->value('submission_id');
}
// Assuming submission file was removed
if (!$submissionId) {
return $sitePrimaryLocale;
}
$contextId = DB::table('submissions')->where('submission_id', $submissionId)->value('context_id');
if (!$contextId) {
return $sitePrimaryLocale;
}
return $contextIdPrimaryLocaleMap[$contextId];
}
/**
* Rename setting name to avoid ambiguity in the event log schema
*/
protected function renameSettings()
{
// First remove 'originalFileName' setting where 'name' setting exists
$idsToDelete = DB::table('event_log_settings AS es')
->join('event_log AS e', 'es.log_id', '=', 'e.log_id')
->whereIn('e.event_type', [
0x50000008, // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD,
0x50000010, // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_EDIT
])
->where('es.setting_name', 'name')
->pluck('e.log_id');
foreach ($idsToDelete->chunk(self::CHUNK_SIZE) as $ids) {
DB::table('event_log_settings')
->whereIn('log_id', $ids->toArray())
->where('setting_name', 'originalFileName')
->delete();
}
// Perform setting renaming
foreach ($this->mapSettings() as $eventType => $settings) {
$idsToUpdate = DB::table('event_log')
->where('event_type', $eventType)
->pluck('log_id');
foreach ($settings as $oldSettingName => $newSettingName) {
foreach ($idsToUpdate->chunk(self::CHUNK_SIZE) as $ids) {
DB::table('event_log_settings')
->whereIn('log_id', $ids->toArray())
->where('setting_name', $oldSettingName)
->update(['setting_name' => $newSettingName]);
}
}
}
}
/**
* Map of new setting names for the event log
* event type => [
* old setting => new setting
* ]
*/
protected function mapSettings(): Collection
{
return collect([
0x10000009 => [ // PKPSubmissionEventLogEntry::SUBMISSION_LOG_COPYRIGHT_AGREED
'name' => 'userFullName'
],
0x40000019 => [ // PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_CONFIRMED
'userName' => 'editorName'
],
0x40000011 => [ // PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_SET_DUE_DATE
'dueDate' => 'reviewDueDate'
],
0x50000001 => [ // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_UPLOAD
'originalFileName' => 'filename'
],
/**
* 'originalFileName' and 'name' are duplicate entries in some events, the former arises from the times before
* submission files refactoring, where it had pointed to the name of the original name of the uploaded file
* rather than the user defined localized name.
* Keep the 'name' where it exists, otherwise preserve 'originalFileName'
*/
0x50000008 => [ // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_REVISION_UPLOAD
'name' => 'filename',
'originalFileName' => 'filename'
],
0x50000010 => [ // SubmissionFileEventLogEntry::SUBMISSION_LOG_FILE_EDIT
'name' => 'filename',
'originalFileName' => 'filename'
],
0x10000003 => [ // PKPSubmissionEventLogEntry::SUBMISSION_LOG_ADD_PARTICIPANT
'name' => 'userFullName'
],
0x10000004 => [ // PKPSubmissionEventLogEntry::SUBMISSION_LOG_REMOVE_PARTICIPANT
'name' => 'userFullName'
]
]);
}
/**
* Reverse the migration.
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I8992_FixEmptyUrlPaths.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 I8992_FixEmptyUrlPaths
*
* @brief Standardize the url column to hold NULL instead of NULL/empty string.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
abstract class I8992_FixEmptyUrlPaths extends \PKP\migration\Migration
{
/**
* Retrieve the tables and fields to update
* @return string[]
*/
protected function getFieldset(): array
{
return [['publications', 'url_path']];
}
/**
* Run the migration
*/
public function up(): void
{
foreach ($this->getFieldset() as [$table, $column]) {
DB::table($table)->whereRaw("TRIM({$column}) = ''")->update([$column => null]);
}
}
/**
* Reverse the migration
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,50 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I9039_DropDeprecatedFields.php
*
* Copyright (c) 2023 Simon Fraser University
* Copyright (c) 2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I9039_DropDeprecatedFields
*
* @brief Drop deprecated fields
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I9039_DropDeprecatedFields extends Migration
{
/**
* Run the migration.
*/
public function up(): void
{
$fieldMap = [
'user_settings' => ['assoc_id', 'assoc_type'],
'review_assignments' => ['reviewer_file_id']
];
foreach ($fieldMap as $entity => $columns) {
foreach ($columns as $column) {
if (Schema::hasColumn($entity, $column)) {
Schema::dropColumns($entity, $column);
}
}
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I9040_DropSettingType.php
*
* Copyright (c) 2023 Simon Fraser University
* Copyright (c) 2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I9040_DropSettingType
*
* @brief Drop not needed setting_type fields
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I9040_DropSettingType extends Migration
{
/**
* Retrieve the affected entities
* @return string[]
*/
protected function getEntities(): array
{
return ['announcement_settings', 'submission_file_settings'];
}
/**
* Run the migration.
*/
public function up(): void
{
foreach ($this->getEntities() as $table) {
if (Schema::hasColumn($table, 'setting_type')) {
Schema::dropColumns($table, 'setting_type');
}
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I9136_MigrateUniqueSiteId.php
*
* Copyright (c) 2023 Simon Fraser University
* Copyright (c) 2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class I9136_MigrateUniqueSiteId
*
* @brief Migrate UsageEvent plugin setting 'uniqueSiteId' to the site_settings table.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\core\PKPString;
use PKP\install\DowngradeNotSupportedException;
class I9136_MigrateUniqueSiteId extends \PKP\migration\Migration
{
/**
* Run the migrations
*/
public function up(): void
{
$newUniqueSiteId = DB::table('site_settings')
->where('setting_name', '=', 'uniqueSiteId')
->value('setting_value');
if (is_null($newUniqueSiteId)) {
$oldUniqueSiteId = DB::table('plugin_settings')
->where('plugin_name', '=', 'usageeventplugin')
->where('setting_name', '=', 'uniqueSiteId')
->value('setting_value');
$uniqueSiteId = is_null($oldUniqueSiteId) || strlen($oldUniqueSiteId) == 0 ? PKPString::generateUUID() : $oldUniqueSiteId;
DB::table('site_settings')->insert([
'setting_name' => 'uniqueSiteId',
'setting_value' => $uniqueSiteId
]);
DB::table('plugin_settings')
->where('plugin_name', '=', 'usageeventplugin')
->where('setting_name', '=', 'uniqueSiteId')
->delete();
}
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I9535_FixEmptyFileStage.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 I9535_FixEmptyFileStage.php
*
* @brief Redirect empty file stages to the SUBMISSION_FILE_SUBMISSION.
*
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Support\Facades\DB;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I9535_FixEmptyFileStage extends Migration
{
public function up(): void
{
DB::table('submission_files')
// empty file_stage
->where('file_stage', 0)
// To \PKP\submissionFile\SubmissionFile::SUBMISSION_FILE_SUBMISSION
->update(['file_stage' => 2]);
}
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,72 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/I9627_AddUsageStatsTemporaryTablesIndexes.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 I9627_AddUsageStatsTemporaryTablesIndexes
*
* @brief Add an index to temporary usage stats tables to fix/improve the removeDoubleClicks and compileUniqueClicks query.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
class I9627_AddUsageStatsTemporaryTablesIndexes extends Migration
{
public $ipTables = [
'usage_stats_total_temporary_records',
'usage_stats_unique_item_investigations_temporary_records',
'usage_stats_unique_item_requests_temporary_records',
];
/**
* Run the migration.
*/
public function up(): void
{
// Decrease the size of the column ip to 64 characters
foreach ($this->ipTables as $ipTable) {
Schema::table($ipTable, function (Blueprint $table) {
$table->string('ip', 64)->change();
});
}
$sm = Schema::getConnection()->getDoctrineSchemaManager();
Schema::table('usage_stats_total_temporary_records', function (Blueprint $table) use ($sm) {
$indexesFound = $sm->listTableIndexes('usage_stats_total_temporary_records');
if (!array_key_exists('ust_load_id_context_id_ip', $indexesFound)) {
$table->index(['load_id', 'context_id', 'ip'], 'ust_load_id_context_id_ip');
}
});
Schema::table('usage_stats_unique_item_investigations_temporary_records', function (Blueprint $table) use ($sm) {
$indexesFound = $sm->listTableIndexes('usage_stats_unique_item_investigations_temporary_records');
if (!array_key_exists('usii_load_id_context_id_ip', $indexesFound)) {
$table->index(['load_id', 'context_id', 'ip'], 'usii_load_id_context_id_ip');
}
});
Schema::table('usage_stats_unique_item_requests_temporary_records', function (Blueprint $table) use ($sm) {
$indexesFound = $sm->listTableIndexes('usage_stats_unique_item_requests_temporary_records');
if (!array_key_exists('usir_load_id_context_id_ip', $indexesFound)) {
$table->index(['load_id', 'context_id', 'ip'], 'usir_load_id_context_id_ip');
}
});
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
}
@@ -0,0 +1,138 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/InstallEmailTemplates.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 InstallEmailTemplates
*
* @brief Install all new email templates for 3.4.
*/
namespace PKP\migration\upgrade\v3_4_0;
use APP\core\Services;
use Exception;
use Illuminate\Support\Facades\DB;
use PKP\db\XMLDAO;
use PKP\facades\Locale;
use PKP\migration\Migration;
abstract class InstallEmailTemplates extends Migration
{
protected array $emailTemplatesInstalled = [];
abstract protected function getEmailTemplateKeys(): array;
abstract protected function getAppVariableNames(): array;
public function up(): void
{
$xmlDao = new XMLDAO();
$data = $xmlDao->parseStruct('registry/emailTemplates.xml', ['email']);
if (!isset($data['email'])) {
throw new Exception('Unable to find <email> entries in registry/emailTemplates.xml.');
}
$contextIds = Services::get('context')->getIds();
$locales = json_decode(
DB::table('site')->pluck('installed_locales')->first()
);
foreach ($data['email'] as $entry) {
$attrs = $entry['attributes'];
if (!in_array($attrs['key'], $this->getEmailTemplateKeys())) {
continue;
}
if (DB::table('email_templates_default_data')->where('email_key', $attrs['key'])->exists()) {
continue;
}
$name = $attrs['name'] ?? null;
$subject = $attrs['subject'] ?? null;
$body = $attrs['body'] ?? null;
if (!$name || !$subject || !$body) {
throw new Exception('Failed to install email template ' . $attrs['key'] . '. Missing name, subject or body attribute.');
}
$previous = Locale::getMissingKeyHandler();
Locale::setMissingKeyHandler(fn (string $key): string => '');
foreach ($locales as $locale) {
$translatedName = $name ? __($name, [], $locale) : $attrs['key'];
$translatedSubject = __($subject, [], $locale);
$translatedBody = __($body, [], $locale);
DB::table('email_templates_default_data')->insert([
'email_key' => $attrs['key'],
'locale' => $locale,
'name' => $translatedName,
'subject' => $this->renameApplicationVariables($translatedSubject),
'body' => $this->renameApplicationVariables($translatedBody),
]);
}
$this->emailTemplatesInstalled[] = $attrs['key'];
Locale::setMissingKeyHandler($previous);
if (isset($attrs['alternateTo'])) {
$exists = DB::table('email_templates_default_data')
->where('email_key', $attrs['alternateTo'])
->exists();
if (!$exists) {
throw new Exception('Tried to install email template `' . $attrs['key'] . '` as an alternate to `' . $attrs['alternateTo'] . '`, but no default template exists with this key.');
}
foreach ($contextIds as $contextId) {
DB::table('email_templates')->insert([
'email_key' => $attrs['key'],
'context_id' => $contextId,
'alternate_to' => $attrs['alternateTo'],
]);
}
}
}
}
public function down(): void
{
DB::table('email_templates_default_data')
->whereIn('email_key', $this->emailTemplatesInstalled)
->delete();
DB::table('email_templates')
->whereIn('email_key', $this->emailTemplatesInstalled)
->delete();
}
/**
* Rename the email template variables that are app-specific
*
* @param string $string Email template subject or body to be modified
*/
protected function renameApplicationVariables(string $string): string
{
$map = $this->getAppVariableNames();
if (empty($map)) {
return $string;
}
$variables = [];
$replacements = [];
foreach ($map as $key => $value) {
$variables[] = '/\{\$' . $key . '\}/';
$replacements[] = '{$' . $value . '}';
}
return preg_replace($variables, $replacements, $string);
}
}
@@ -0,0 +1,464 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/MergeLocalesMigration.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 MergeLocalesMigration
*
* @brief Change Locales from locale_countryCode localization folder notation to locale localization folder notation
*/
namespace PKP\migration\upgrade\v3_4_0;
use Exception;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\db\DAORegistry;
use PKP\install\DowngradeNotSupportedException;
abstract class MergeLocalesMigration extends \PKP\migration\Migration
{
protected string $CONTEXT_TABLE = '';
protected string $CONTEXT_SETTINGS_TABLE = '';
protected string $CONTEXT_COLUMN = '';
/**
* Run the migrations.
*/
public function up(): void
{
// This change fixes the locale column length on the static_page_settings
// which in 3.1.0 version was varchar(5).
if (Schema::hasTable('static_page_settings')) {
Schema::table('static_page_settings', function (Blueprint $table) {
$table->string('locale', 14)->change();
});
}
if (empty($this->CONTEXT_TABLE) || empty($this->CONTEXT_SETTINGS_TABLE) || empty($this->CONTEXT_COLUMN)) {
throw new Exception('Upgrade could not be completed because required properties for the MergeLocalesMigration migration are undefined.');
}
// All _settings tables.
foreach ($this->getSettingsTables() as $tableName => [$entityIdColumnName, $primaryKeyColumnName]) {
if (!Schema::hasTable($tableName) || !Schema::hasColumn($tableName, 'locale')) {
continue;
}
foreach (self::getAffectedLocales() as $source => $target) {
DB::table($tableName)
->where('locale', $source)
->update(['locale' => $target]);
}
}
// Tables
// site
$site = DB::table('site')
->select(['supported_locales', 'installed_locales', 'primary_locale'])
->first();
$this->updateArrayLocaleNoId($site->supported_locales, 'site', 'supported_locales');
$this->updateArrayLocaleNoId($site->installed_locales, 'site', 'installed_locales');
$this->updateSingleValueLocaleNoId($site->primary_locale, 'site', 'primary_locale');
Schema::table('site', function (Blueprint $table) {
$table->string('installed_locales')->default('en')->change();
});
// users
$migration = $this;
$users = DB::table('users')->chunkById(1000, function ($users) use ($migration) {
foreach ($users as $user) {
$migration->updateArrayLocale($user->locales, 'users', 'locales', 'user_id', $user->user_id);
}
}, 'user_id');
// submissions
$submissions = DB::table('submissions')->chunkById(1000, function ($submissions) use ($migration) {
foreach ($submissions as $submission) {
$migration->updateSingleValueLocale($submission->locale, 'submissions', 'locale', 'submission_id', $submission->submission_id);
}
}, 'submission_id');
// email_templates_default_data
$emailTemplatesDefaultData = DB::table('email_templates_default_data')
->get();
foreach ($emailTemplatesDefaultData as $emailTemplatesDefaultDataCurrent) {
$this->updateSingleValueLocaleEmailData($emailTemplatesDefaultDataCurrent->locale, 'email_templates_default_data', $emailTemplatesDefaultDataCurrent->email_key, $emailTemplatesDefaultData);
}
Schema::table('email_templates_default_data', function (Blueprint $table) {
$table->string('locale')->default('en')->change();
});
// Context
$contexts = DB::table($this->CONTEXT_TABLE)
->get();
foreach ($contexts as $context) {
$this->updateSingleValueLocale($context->primary_locale, $this->CONTEXT_TABLE, 'primary_locale', $this->CONTEXT_COLUMN, $context->{$this->CONTEXT_COLUMN});
}
$contextSettingsFormLocales = DB::table($this->CONTEXT_SETTINGS_TABLE)
->where('setting_name', '=', 'supportedFormLocales')
->get();
foreach ($contextSettingsFormLocales as $contextSettingsFormLocale) {
$this->updateArrayLocaleSetting($contextSettingsFormLocale->setting_value, $this->CONTEXT_SETTINGS_TABLE, 'supportedFormLocales', $this->CONTEXT_COLUMN, $contextSettingsFormLocale->{$this->CONTEXT_COLUMN});
}
$contextSettingsFormLocales = DB::table($this->CONTEXT_SETTINGS_TABLE)
->where('setting_name', '=', 'supportedLocales')
->get();
foreach ($contextSettingsFormLocales as $contextSettingsFormLocale) {
$this->updateArrayLocaleSetting($contextSettingsFormLocale->setting_value, $this->CONTEXT_SETTINGS_TABLE, 'supportedLocales', $this->CONTEXT_COLUMN, $contextSettingsFormLocale->{$this->CONTEXT_COLUMN});
}
$contextSettingsFormLocales = DB::table($this->CONTEXT_SETTINGS_TABLE)
->where('setting_name', '=', 'supportedSubmissionLocales')
->get();
foreach ($contextSettingsFormLocales as $contextSettingsFormLocale) {
$this->updateArrayLocaleSetting($contextSettingsFormLocale->setting_value, $this->CONTEXT_SETTINGS_TABLE, 'supportedSubmissionLocales', $this->CONTEXT_COLUMN, $contextSettingsFormLocale->{$this->CONTEXT_COLUMN});
}
// plugin_settings
// customBlockManager
$blockPluginName = 'customblockmanagerplugin';
$blockLocalizedSettingNames = ['blockTitle', 'blockContent'];
$contextIds = DB::table($this->CONTEXT_TABLE)
->get()
->pluck($this->CONTEXT_COLUMN);
foreach ($contextIds as $contextId) {
$blocks = DB::table('plugin_settings')
->where('plugin_name', '=', $blockPluginName)
->where('setting_name', '=', 'blocks')
->where('context_id', '=', $contextId)
->get()
->pluck('setting_value');
if (!$blocks->isEmpty()) {
$blocksArray = json_decode($blocks[0], true);
foreach ($blocksArray as $block) {
foreach ($blockLocalizedSettingNames as $blockLocalizedSettingName) {
$blockLocalizedContent = DB::table('plugin_settings')
->where('plugin_name', '=', $block)
->where('setting_name', '=', $blockLocalizedSettingName)
->where('context_id', '=', $contextId)
->first();
if (isset($blockLocalizedContent)) {
$this->updateArrayKeysLocaleSetting($blockLocalizedContent->setting_value, 'plugin_settings', 'plugin_setting_id', $blockLocalizedContent->plugin_setting_id);
}
}
}
$pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */
$cache = $pluginSettingsDao->_getCache($contextId, $blockPluginName);
$cache->flush();
}
}
}
public function updateArrayLocaleNoId(string $dbLocales, string $table, string $column)
{
$siteSupportedLocales = json_decode($dbLocales) ?? [];
if ($siteSupportedLocales !== false) {
$newLocales = [];
foreach ($siteSupportedLocales as $siteSupportedLocale) {
$updatedLocale = $this->getUpdatedLocale($siteSupportedLocale);
if (!is_null($updatedLocale)) {
if (!in_array($updatedLocale, $newLocales)) {
$newLocales[] = $updatedLocale;
}
} else {
$newLocales[] = $siteSupportedLocale;
}
}
DB::table($table)
->update([
$column => $newLocales
]);
}
}
public function updateArrayLocale(string $dbLocales, string $table, string $column, string $tableKeyColumn, int $id)
{
$siteSupportedLocales = json_decode($dbLocales) ?? [];
$newLocales = [];
foreach ($siteSupportedLocales as $siteSupportedLocale) {
$updatedLocale = $this->getUpdatedLocale($siteSupportedLocale);
if (!is_null($updatedLocale)) {
if (!in_array($updatedLocale, $newLocales)) {
$newLocales[] = $updatedLocale;
}
} else {
$newLocales[] = $siteSupportedLocale;
}
}
DB::table($table)
->where($tableKeyColumn, '=', $id)
->update([
$column => $newLocales
]);
}
public function updateArrayLocaleSetting(string $dbLocales, string $table, string $settingValue, string $tableKeyColumn, int $id)
{
$siteSupportedLocales = json_decode($dbLocales) ?? [];
if ($siteSupportedLocales !== false) {
$newLocales = [];
foreach ($siteSupportedLocales as $siteSupportedLocale) {
$updatedLocale = $this->getUpdatedLocale($siteSupportedLocale);
if (!is_null($updatedLocale)) {
if (!in_array($updatedLocale, $newLocales)) {
$newLocales[] = $updatedLocale;
}
} else {
$newLocales[] = $siteSupportedLocale;
}
}
DB::table($table)
->where($tableKeyColumn, '=', $id)
->where('setting_name', '=', $settingValue)
->update([
'setting_value' => $newLocales
]);
}
}
public function updateArrayKeysLocaleSetting(string $dbLocales, string $table, string $tableKeyColumn, int $id)
{
$siteSupportedLocales = json_decode($dbLocales, true) ?? [];
if ($siteSupportedLocales !== false) {
$newLocales = [];
foreach (array_keys($siteSupportedLocales) as $siteSupportedLocaleKey) {
$updatedLocale = $this->getUpdatedLocale($siteSupportedLocaleKey);
if (!is_null($updatedLocale)) {
$newLocales[$updatedLocale] = $siteSupportedLocales[$siteSupportedLocaleKey];
} else {
$newLocales[$siteSupportedLocaleKey] = $siteSupportedLocales[$siteSupportedLocaleKey];
}
}
$jsonString = json_encode($newLocales);
DB::table($table)
->where($tableKeyColumn, '=', $id)
->update([
'setting_value' => $jsonString
]);
}
}
public function updateSingleValueLocale(?string $localevalue, string $table, string $column, string $tableKeyColumn, int $id)
{
if ($localevalue === null) {
return;
}
$updatedLocale = $this->getUpdatedLocale($localevalue);
if (!is_null($updatedLocale)) {
DB::table($table)
->where($tableKeyColumn, '=', $id)
->update([
$column => $updatedLocale
]);
}
}
public function updateSingleValueLocaleNoId(string $localevalue, string $table, string $column)
{
$updatedLocale = $this->getUpdatedLocale($localevalue);
if (!is_null($updatedLocale)) {
DB::table($table)
->update([
$column => $updatedLocale
]);
}
}
public function updateSingleValueLocaleEmailData(string $localevalue, string $table, string $email_key, Collection $allEmailTemplateData)
{
$localeRows = DB::table($table)
->where('email_key', '=', $email_key)
->where('locale', '=', $localevalue);
$stillExists = $localeRows->exists();
if ($stillExists) {
$updatedLocale = $this->getUpdatedLocale($localevalue);
// if this is null we should do nothing - we are not handling this locale
if (!is_null($updatedLocale)) {
// if the updatedLocale is the same as the setting's locale we should do nothing
// Check if the database already has an updated locale with the same value -
$hasAlreadyExistingUpdatedLocale = DB::table($table)
->where('email_key', '=', $email_key)
->where('locale', '=', $updatedLocale)
->exists();
// if so, it is safe to delete the currently processed value.
if ($hasAlreadyExistingUpdatedLocale) {
$localeRows->delete();
} else {
$localeRows->update(['locale' => $updatedLocale]);
}
}
}
}
/**
* Returns null if no conversion is available or
* a key value pair collection that the key is the output locale and the value is the defaultLocale.
*/
public function getUpdatedLocale(string $localeValue): ?string
{
$affectedLocales = $this->getAffectedLocales();
if ($affectedLocales->keys()->contains($localeValue)) {
$localeTransformation = $affectedLocales[$localeValue];
return $localeTransformation;
}
return null;
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
/**
* Get a list of settings tables, keyed by table name. Values are [entity_id_column_name, settings_table_id_column_name].
*/
protected static function getSettingsTables(): Collection
{
return collect([
'announcement_settings' => ['announcement_id', 'announcement_setting_id'],
'announcement_type_settings' => ['type_id', 'announcement_type_setting_id'],
'author_settings' => ['author_id', 'author_setting_id'],
'category_settings' => ['category_id', 'category_setting_id'],
'citation_settings' => ['citation_id', 'citation_setting_id'],
'controlled_vocab_entry_settings' => ['controlled_vocab_entry_id', 'controlled_vocab_entry_setting_id'],
'data_object_tombstone_settings' => ['tombstone_id', 'tombstone_setting_id'],
'email_templates_settings' => ['email_id', 'email_template_setting_id'],
'event_log_settings' => ['log_id', 'event_log_setting_id'],
'filter_settings' => ['filter_id', 'filter_setting_id'],
'genre_settings' => ['genre_id', 'genre_setting_id'],
'library_file_settings' => ['file_id', 'library_file_setting_id'],
'navigation_menu_item_assignment_settings' => ['navigation_menu_item_assignment_id', 'navigation_menu_item_assignment_setting_id'],
'navigation_menu_item_settings' => ['navigation_menu_item_id', 'navigation_menu_item_setting_id'],
'notification_settings' => ['notification_id', 'notification_setting_id'],
'notification_subscription_settings' => ['setting_id', 'notification_subscription_setting_id'],
'plugin_settings' => ['context_id', 'plugin_setting_id'],
'publication_settings' => ['publication_id', 'publication_setting_id'],
'review_form_element_settings' => ['review_form_element_id', 'review_form_element_setting_id'],
'review_form_settings' => ['review_form_id', 'review_form_setting_id'],
'submission_file_settings' => ['submission_file_id', 'submission_file_setting_id'],
'submission_settings' => ['submission_id', 'submission_setting_id'],
'user_group_settings' => ['user_group_id', 'user_group_setting_id'],
'user_settings' => ['user_id', 'user_setting_id'],
'site_settings' => [null, 'site_setting_id'],
'funder_settings' => ['funder_id', 'funder_setting_id'],
'funder_award_settings' => ['funder_award_id', 'funder_award_setting_id'],
'static_page_settings' => ['static_page_id', 'static_page_setting_id'],
]);
}
/**
* Returns the effected locales along with the corresponding rename for each
*/
public static function getAffectedLocales(): Collection
{
return collect([
'es_ES' => 'es',
'en_US' => 'en',
'sr_RS@cyrillic' => 'sr@cyrillic',
'sr_RS@latin' => 'sr@latin',
'el_GR' => 'el',
'de_DE' => 'de',
'da_DK' => 'da',
'cs_CZ' => 'cs',
'ca_ES' => 'ca',
'bs_BA' => 'bs',
'bg_BG' => 'bg',
'be_BY@cyrillic' => 'be@cyrillic',
'az_AZ' => 'az',
'ar_IQ' => 'ar',
'fa_IR' => 'fa',
'fi_FI' => 'fi',
'gd_GB' => 'gd',
'gl_ES' => 'gl',
'he_IL' => 'he',
'hi_IN' => 'hi',
'hr_HR' => 'hr',
'hu_HU' => 'hu',
'hy_AM' => 'hy',
'id_ID' => 'id',
'is_IS' => 'is',
'it_IT' => 'it',
'ja_JP' => 'ja',
'ka_GE' => 'ka',
'kk_KZ' => 'kk',
'ko_KR' => 'ko',
'ku_IQ' => 'ku',
'lt_LT' => 'lt',
'lv_LV' => 'lv',
'mk_MK' => 'mk',
'mn_MN' => 'mn',
'ms_MY' => 'ms',
'nb_NO' => 'nb',
'nl_NL' => 'nl',
'pl_PL' => 'pl',
'ro_RO' => 'ro',
'ru_RU' => 'ru',
'si_LK' => 'si',
'sk_SK' => 'sk',
'sl_SI' => 'sl',
'sv_SE' => 'sv',
'tr_TR' => 'tr',
'uk_UA' => 'uk',
'ur_PK' => 'ur',
'uz_UZ@cyrillic' => 'uz@cyrillic',
'uz_UZ@latin' => 'uz@latin',
'vi_VN' => 'vi',
'eu_ES' => 'eu',
'sw_KE' => 'sw',
'zh_TW' => 'zh_Hant'
]);
}
}
@@ -0,0 +1,411 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/PKPI7014_DoiMigration.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 PKPI7014_DoiMigration
*
* @brief Migrate DOI related fields to the new structures
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use PKP\install\DowngradeNotSupportedException;
use PKP\migration\Migration;
abstract class PKPI7014_DoiMigration extends Migration
{
/**
* @inheritDoc
*/
public function up(): void
{
// DOIs
Schema::create('dois', function (Blueprint $table) {
$table->bigInteger('doi_id')->autoIncrement();
$table->bigInteger('context_id');
$table->foreign('context_id')->references($this->getContextIdColumn())->on($this->getContextTable());
$table->index(['context_id'], 'dois_context_id');
$table->string('doi');
$table->smallInteger('status')->default(1);
});
// Settings
Schema::create('doi_settings', function (Blueprint $table) {
$table->bigIncrements('doi_setting_id');
$table->bigInteger('doi_id');
$table->foreign('doi_id')->references('doi_id')->on('dois')->cascadeOnDelete();
$table->index(['doi_id'], 'doi_settings_doi_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['doi_id', 'locale', 'setting_name'], 'doi_settings_unique');
});
// Add doiId to publication
Schema::table('publications', function (Blueprint $table) {
$table->bigInteger('doi_id')->nullable();
$table->foreign('doi_id')->references('doi_id')->on('dois')->nullOnDelete();
$table->index(['doi_id'], 'publications_doi_id');
});
}
/**
* Reverse the downgrades
*
* @throws DowngradeNotSupportedException
*/
public function down(): void
{
throw new DowngradeNotSupportedException();
}
protected function migrateExistingDataUp(): void
{
$this->_migrateDoiSettingsToContext();
$this->_transferDefaultPatterns();
$this->_migratePublicationDoisUp();
}
/**
* Move DOI settings from plugin_settings to Context (Journal/Press/Server) settings
*/
protected function _migrateDoiSettingsToContext(): void
{
// Get plugin_based settings
$q = DB::table('plugin_settings')
->where('plugin_name', '=', 'doipubidplugin')
->where('context_id', '<>', 0)
->select(['context_id','setting_name', 'setting_value']);
$results = $q->get();
$data = new \stdClass();
$data->enabledDois = [];
$data->doiCreationTime = [];
$data->enabledDoiTypes = [];
$data->doiPrefix = [];
$data->doiSuffixType = [];
$data->doiPublicationSuffixPattern = [];
$data->doiRepresentationSuffixPattern = [];
$data = $this->addSuffixPatternsData($data);
// Map to context-based settings
$results->reduce(function ($carry, $item) {
switch ($item->setting_name) {
case 'enabled':
$carry->enabledDois[] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => 'enableDois',
'setting_value' => (int) $item->setting_value,
];
$carry->doiCreationTime[] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => 'doiCreationTime',
'setting_value' => 'copyEditCreationTime',
];
return $carry;
case 'enablePublicationDoi':
if (!isset($carry->enabledDoiTypes[$item->context_id])) {
$carry->enabledDoiTypes[$item->context_id] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => 'enabledDoiTypes',
'setting_value' => [],
];
}
if ($item->setting_value === '1') {
array_push($carry->enabledDoiTypes[$item->context_id]['setting_value'], 'publication');
}
return $carry;
case 'enableRepresentationDoi':
if (!isset($carry->enabledDoiTypes[$item->context_id])) {
$carry->enabledDoiTypes[$item->context_id] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => 'enabledDoiTypes',
'setting_value' => [],
];
}
if ($item->setting_value === '1') {
array_push($carry->enabledDoiTypes[$item->context_id]['setting_value'], 'representation');
}
return $carry;
case 'doiSuffix':
$value = '';
switch ($item->setting_value) {
case 'default':
$value = 'default';
break;
case 'pattern':
$value = 'customPattern';
break;
case 'customId':
$value = 'customId';
break;
}
$carry->doiSuffixType[] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => 'doiSuffixType',
'setting_value' => $value,
];
return $carry;
case 'doiPrefix':
case 'doiPublicationSuffixPattern':
case 'doiRepresentationSuffixPattern':
$carry->{$item->setting_name}[] = [
$this->getContextIdColumn() => $item->context_id,
'setting_name' => $item->setting_name,
'setting_value' => $item->setting_value,
];
return $carry;
default:
$carry = $this->insertSuffixPatternsData($carry, $item);
$carry = $this->insertEnabledDoiTypes($carry, $item);
return $carry;
}
}, $data);
// Prepare insert statements
$insertData = [];
foreach ($data->enabledDois as $item) {
array_push($insertData, $item);
}
foreach ($data->doiCreationTime as $item) {
array_push($insertData, $item);
}
foreach ($data->enabledDoiTypes as $item) {
$item['setting_value'] = json_encode($item['setting_value']);
array_push($insertData, $item);
}
foreach ($data->doiPrefix as $item) {
array_push($insertData, $item);
}
foreach ($data->doiSuffixType as $item) {
array_push($insertData, $item);
}
foreach ($data->doiPublicationSuffixPattern as $item) {
array_push($insertData, $item);
}
foreach ($data->doiRepresentationSuffixPattern as $item) {
array_push($insertData, $item);
}
$insertData = $this->prepareSuffixPatternsForInsert($data, $insertData);
DB::table($this->getContextSettingsTable())->insert($insertData);
// Add minimum required DOI settings to context if DOI plugin not previously enabled
$missingDoiSettingsInsertStatement = DB::table($this->getContextTable())
->select($this->getContextIdColumn())
->whereNotIn($this->getContextIdColumn(), function (Builder $q) {
$q->select($this->getContextIdColumn())
->from($this->getContextSettingsTable())
->where('setting_name', '=', 'enableDois');
})
->get()
->reduce(function ($carry, $item) {
$carry[] = [
$this->getContextIdColumn() => $item->{$this->getContextIdColumn()},
'setting_name' => 'enableDois',
'setting_value' => 0,
];
$carry[] = [
$this->getContextIdColumn() => $item->{$this->getContextIdColumn()},
'setting_name' => 'doiCreationTime',
'setting_value' => 'copyEditCreationTime'
];
$carry[] = [
$this->getContextIdColumn() => $item->{$this->getContextIdColumn()},
'setting_name' => 'useDefaultDoiSuffix',
'setting_value' => 1
];
return $carry;
}, []);
DB::table($this->getContextSettingsTable())->insert($missingDoiSettingsInsertStatement);
// Cleanup old DOI plugin settings
DB::table('plugin_settings')
->where('plugin_name', '=', 'doipubidplugin')
->delete();
DB::table('versions')
->where('product_type', '=', 'plugins.pubIds')
->where('product', '=', 'doi')
->delete();
}
/**
* Copies default patterns to custom pattern fields if no custom patterns have been supplied
*/
private function _transferDefaultPatterns(): void
{
// Collect list of all possible custom suffix types for each context
$suffixPatternStatuses = DB::table($this->getContextTable())
->select($this->getContextIdColumn())
->get()
->reduce(function ($carry, $item) {
$carry[$item->{$this->getContextIdColumn()}] = [];
foreach ($this->getSuffixPatternNames() as $suffixPatternName) {
$carry[$item->{$this->getContextIdColumn()}][$suffixPatternName] = false;
}
return $carry;
}, []);
// Flag those suffix types for each context that are already stored in the database
DB::table($this->getContextSettingsTable())
->whereIn('setting_name', function (Builder $q) {
$q->select('setting_name')
->from($this->getContextSettingsTable())
->whereIn('setting_name', $this->getSuffixPatternNames())
// We don't want to flag null suffix patterns as existing, otherwise the default
// pattern would not be added
->whereNotNull('setting_value');
})
->get()
->each(function ($item) use (&$suffixPatternStatuses) {
$suffixPatternStatuses[$item->{$this->getContextIdColumn()}][$item->setting_name] = true;
});
// Prepare insert statement to add default suffix patterns as custom patterns
// where no previous custom pattern existed
$insertStatements = [];
foreach ($suffixPatternStatuses as $contextId => $suffixPatternStatus) {
foreach ($suffixPatternStatus as $suffixPattern => $status) {
if ($status) {
continue;
}
$insertStatements[] = [
$this->getContextIdColumn() => $contextId,
'setting_name' => $suffixPattern,
'setting_value' => $this->getSuffixPatternValue($suffixPattern),
];
}
}
// Finally, save custom patterns to DB
if (!empty($insertStatements)) {
DB::table($this->getContextSettingsTable())->upsert($insertStatements, [$this->getContextIdColumn(), 'locale', 'setting_name'], ['setting_value']);
}
}
/**
* Move publication DOIs from publication_settings table to DOI objects
*/
private function _migratePublicationDoisUp(): void
{
$q = DB::table('submissions', 's')
->select(['s.context_id', 'p.publication_id', 'p.doi_id', 'pss.setting_name', 'pss.setting_value'])
->leftJoin('publications as p', 'p.submission_id', '=', 's.submission_id')
->leftJoin('publication_settings as pss', 'pss.publication_id', '=', 'p.publication_id')
->where('pss.setting_name', '=', 'pub-id::doi');
$q->chunkById(1000, function ($items) {
foreach ($items as $item) {
// Double-check to ensure a DOI object does not already exist for publication
if ($item->doi_id === null) {
$doiId = $this->_addDoi($item->context_id, $item->setting_value);
// Add association to newly created DOI to publication
DB::table('publications')
->where('publication_id', '=', $item->publication_id)
->update(['doi_id' => $doiId]);
} else {
// Otherwise, update existing DOI object
$this->_updateDoi($item->doi_id, $item->context_id, $item->setting_value);
}
}
}, 'p.publication_id', 'publication_id');
// Remove pub-id::doi settings entry
DB::table('publication_settings')
->where('setting_name', '=', 'pub-id::doi')
->delete();
}
/**
* Creates a new DOI object for a given context ID and DOI
*/
protected function _addDoi(string $contextId, string $doi): int
{
return DB::table('dois')
->insertGetId(
[
'context_id' => $contextId,
'doi' => $doi,
], 'doi_id'
);
}
/**
* Update the context ID and doi for a given DOI object
*/
protected function _updateDoi(int $doiId, string $contextId, string $doi): int
{
return DB::table('dois')
->where('doi_id', '=', $doiId)
->update(
[
'context_id' => $contextId,
'doi' => $doi
]
);
}
/**
* Gets app-specific context table name, e.g. journals
*/
abstract protected function getContextTable(): string;
/**
* Gets app-specific context_id column, e.g. journal_id
*/
abstract protected function getContextIdColumn(): string;
/**
* Gets app-specific context settings table, e.g. journal_settings
*/
abstract protected function getContextSettingsTable(): string;
/**
* Adds app-specific suffix patterns to data collector stdClass
*/
abstract protected function addSuffixPatternsData(\stdClass $data): \stdClass;
/**
* Adds suffix pattern settings from DB into reducer's data
*/
abstract protected function insertSuffixPatternsData(\stdClass $carry, \stdClass $item): \stdClass;
/**
* Adds insert-ready statements for all applicable suffix pattern items
*/
abstract protected function prepareSuffixPatternsForInsert(\stdClass $processedData, array $insertData): array;
/**
* Add app-specific enabled DOI types for insert into DB
*/
abstract protected function insertEnabledDoiTypes(\stdClass $carry, \stdClass $item): \stdClass;
/**
* Get an array with the keys for each suffix pattern type
*/
abstract protected function getSuffixPatternNames(): array;
/**
* Returns the default pattern for the given suffix pattern type
*/
abstract protected function getSuffixPatternValue(string $suffixPatternName): string;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/UpgradeMigration.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 UpgradeMigration
*
* @brief Describe upgrade/downgrade operations from 3.3.x to 3.4.0.
*/
namespace PKP\migration\upgrade\v3_4_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class UpgradeMigration extends \PKP\migration\Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// pkp/pkp-lib#6093: Delete review_assignment entries that correspond to nonexistent submissions.
$orphanedIds = DB::table('review_assignments AS ra')->leftJoin('submissions AS s', 'ra.submission_id', '=', 's.submission_id')->whereNull('s.submission_id')->pluck('ra.submission_id', 'ra.review_id');
foreach ($orphanedIds as $reviewId => $submissionId) {
$this->_installer->log("Removing orphaned review_assignments entry ID {$reviewId} with submission_id {$submissionId}");
DB::table('review_assignments')->where('review_id', '=', $reviewId)->delete();
}
// pkp/pkp-lib#6093: Delete review_assignment entries that correspond to nonexistent reviewers.
$orphanedIds = DB::table('review_assignments AS ra')->leftJoin('users AS u', 'ra.reviewer_id', '=', 'u.user_id')->whereNull('u.user_id')->pluck('ra.reviewer_id', 'ra.review_id');
foreach ($orphanedIds as $reviewId => $userId) {
$this->_installer->log("Removing orphaned review_assignments entry ID {$reviewId} with reviewer_id {$userId}");
DB::table('review_assignments')->where('review_id', '=', $reviewId)->delete();
}
// pkp/pkp-lib#6093: Delete review_assignment entries that correspond to nonexistent review rounds.
$orphanedIds = DB::table('review_assignments AS ra')->leftJoin('review_rounds AS rr', 'ra.review_round_id', '=', 'rr.review_round_id')->whereNull('rr.review_round_id')->pluck('ra.review_round_id', 'ra.review_id');
foreach ($orphanedIds as $reviewId => $reviewRoundId) {
$this->_installer->log("Removing orphaned review_assignments entry ID {$reviewId} with review_round_id {$reviewRoundId}");
DB::table('review_assignments')->where('review_id', '=', $reviewId)->delete();
}
// pkp/pkp-lib#6093: Delete review_assignment entries that correspond to nonexistent review forms.
$orphanedIds = DB::table('review_assignments AS ra')->leftJoin('review_forms AS rf', 'ra.review_form_id', '=', 'rf.review_form_id')->whereNull('rf.review_form_id')->whereNotNull('ra.review_form_id')->pluck('ra.review_form_id', 'ra.review_id');
foreach ($orphanedIds as $reviewId => $reviewFormId) {
$this->_installer->log("Using default review form for review with ID {$reviewId} which refers to nonexistent review_form_id {$reviewFormId}");
DB::table('review_assignments')->where('review_id', '=', $reviewId)->update(['review_form_id' => null]);
}
// pkp/pkp-lib#6093: Set up foreign key constraints
Schema::table('review_assignments', function (Blueprint $table) {
$table->foreign('submission_id')->references('submission_id')->on('submissions');
$table->foreign('reviewer_id')->references('user_id')->on('users');
$table->foreign('review_round_id')->references('review_round_id')->on('review_rounds');
$table->foreign('review_form_id')->references('review_form_id')->on('review_forms');
// Normally reviewer can't be assigned twice on the same review round.
// HOWEVER, if two reviewer user accounts are subsequently merged, both will keep
// separate review assignments but the reviewer_id will become the same!
// (https://github.com/pkp/pkp-lib/issues/7678)
$table->index(['review_round_id', 'reviewer_id'], 'review_assignment_reviewer_round');
});
// pkp/pkp-lib#6685: Drop old tombstones table in OJS and OPS
Schema::dropIfExists('submission_tombstones');
// pkp/pkp-lib#7246: Allow default null values for the last login date
Schema::table('users', function (Blueprint $table) {
$table->dateTime('date_last_login')->nullable()->change();
});
// pkp/pkp-lib#7246: Remove setting_type in user_settings
if (Schema::hasColumn('user_settings', 'setting_type')) {
Schema::table('user_settings', function (Blueprint $table) {
$table->dropColumn('setting_type');
});
}
// pkp/pkp-lib#8093: Remove setting_type in user_group_settings
if (Schema::hasColumn('user_group_settings', 'setting_type')) {
Schema::table('user_group_settings', function (Blueprint $table) {
$table->dropColumn('setting_type');
});
}
}
/**
* Reverse the downgrades
*/
public function down(): void
{
Schema::table('review_assignments', function (Blueprint $table) {
$table->dropForeign(['reviewer_id']);
$table->dropForeign(['submission_id']);
$table->dropForeign(['review_round_id']);
$table->dropForeign(['review_form_id']);
});
Schema::table('review_assignments', function (Blueprint $table) {
$table->dropIndex('review_assignment_reviewer_round');
});
Schema::table('users', function (Blueprint $table) {
$table->dateTime('date_last_login')->nullable(false)->default(null)->change();
});
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @file classes/migration/upgrade/v3_4_0/jobs/FixRegionCodes.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 FixRegionCodes
*
* @ingroup jobs
*
* @brief Loads the FIPS-ISO mapping for the given country to the temporary table and fixes the wrong region codes, using the temporary table of FIPS-ISO mapping.
* If it is the last job to run, it removes the temporary indexes and the temporary table.
*/
namespace PKP\migration\upgrade\v3_4_0\jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\core\Core;
use PKP\jobs\BaseJob;
class FixRegionCodes extends BaseJob
{
use Batchable;
/** The country ISO code */
protected string $country;
/**
* Create a new job instance.
*/
public function __construct(string $country)
{
parent::__construct();
$this->country = $country;
}
/**
* Execute the job.
*/
public function handle(): void
{
// read the FIPS to ISO mappings for the given country and isert them into the temporary table
$mappings = include Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/regionMapping.php';
foreach ($mappings[$this->country] as $fips => $iso) {
DB::table('region_mapping_tmp')->insert([
'country' => $this->country,
'fips' => $fips,
'iso' => $iso
]);
}
// update region code from FIPS to ISP, according to the entries in the table region_mapping_tmp
// Laravel join+update does not work well with PostgreSQL, so use the direct SQLs
// daily
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
DB::statement('
UPDATE metrics_submission_geo_daily AS gd
SET region = rm.iso
FROM region_mapping_tmp AS rm
WHERE gd.country = rm.country AND gd.region = rm.fips
');
} else {
DB::statement('
UPDATE metrics_submission_geo_daily gd
INNER JOIN region_mapping_tmp rm ON (rm.country = gd.country AND rm.fips = gd.region)
SET gd.region = rm.iso
');
}
// montly
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
DB::statement('
UPDATE metrics_submission_geo_monthly AS gm
SET region = rm.iso
FROM region_mapping_tmp AS rm
WHERE gm.country = rm.country AND gm.region = rm.fips
');
} else {
DB::statement('
UPDATE metrics_submission_geo_monthly gm
INNER JOIN region_mapping_tmp rm ON (rm.country = gm.country AND rm.fips = gm.region)
SET gm.region = rm.iso
');
}
// clear region_mapping_tmp table
DB::table('region_mapping_tmp')->delete();
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @file classes/migration/upgrade/v3_5_0/I9253_SiteAnnouncements.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 I9253_SiteAnnouncements
*/
namespace PKP\migration\upgrade\v3_5_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I9253_SiteAnnouncements extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('announcements', function (Blueprint $table) {
$table->bigInteger('assoc_id')->nullable()->change();
});
Schema::table('announcement_types', function (Blueprint $table) {
$table->bigInteger('context_id')->nullable()->change();
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::table('announcements', function (Blueprint $table) {
$table->bigInteger('assoc_id')->nullable(false)->change();
});
Schema::table('announcement_types', function (Blueprint $table) {
$table->bigInteger('context_id')->nullable(false)->change();
});
}
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/migration/upgrade/v3_5_0/I9262_Highlights.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 I9262_Highlights
*/
namespace PKP\migration\upgrade\v3_5_0;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use PKP\migration\Migration;
class I9262_Highlights extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('highlights', function (Blueprint $table) {
$table->comment('Highlights are featured items that can be presented to users, for example on the homepage.');
$table->bigInteger('highlight_id')->autoIncrement();
$table->bigInteger('context_id')->nullable();
$contextDao = \APP\core\Application::getContextDAO();
$table->foreign('context_id')->references($contextDao->primaryKeyColumn)->on($contextDao->tableName)->onDelete('cascade');
$table->index(['context_id'], 'highlights_context_id');
$table->bigInteger('sequence');
$table->string('url', 2047);
});
Schema::create('highlight_settings', function (Blueprint $table) {
$table->comment('More data about highlights, including localized properties like title and description.');
$table->bigIncrements('highlight_setting_id');
$table->bigInteger('highlight_id');
$table->foreign('highlight_id')->references('highlight_id')->on('highlights')->onDelete('cascade');
$table->index(['highlight_id'], 'highlight_settings_highlight_id');
$table->string('locale', 14)->default('');
$table->string('setting_name', 255);
$table->mediumText('setting_value')->nullable();
$table->unique(['highlight_id', 'locale', 'setting_name'], 'highlight_settings_unique');
});
}
/**
* Reverse the migration.
*/
public function down(): void
{
Schema::drop('highlight_settings');
Schema::drop('highlights');
}
}