From 5a726ebfc1fc908a72516974aca3600333a10a78 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Fri, 16 Aug 2024 17:55:11 +0600 Subject: [PATCH 01/30] pkp/pkp-lib#10292 Controlled Vocab DAO to Eloquent Model --- api/v1/vocabs/PKPVocabController.php | 4 +- .../forms/publication/PKPMetadataForm.php | 4 +- classes/controlledVocab/ControlledVocab.php | 129 +++++++++------- classes/controlledVocab/Repository.php | 127 ++++++++++++++++ classes/core/PKPApplication.php | 2 - classes/facades/Repo.php | 6 + classes/publication/DAO.php | 13 +- classes/submission/SubmissionAgencyDAO.php | 139 ------------------ classes/submission/SubmissionAgencyVocab.php | 78 ++++++++++ .../validation/ValidatorControlledVocab.php | 36 ++--- .../filter/NativeXmlPKPPublicationFilter.php | 6 +- .../filter/PKPPublicationNativeXmlFilter.php | 6 +- tests/classes/publication/PublicationTest.php | 3 +- 13 files changed, 317 insertions(+), 236 deletions(-) create mode 100644 classes/controlledVocab/Repository.php delete mode 100644 classes/submission/SubmissionAgencyDAO.php create mode 100644 classes/submission/SubmissionAgencyVocab.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index a35e10927dd..133f2addb32 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -31,7 +31,7 @@ use PKP\security\authorization\ContextAccessPolicy; use PKP\security\authorization\UserRolesRequiredPolicy; use PKP\security\Role; -use PKP\submission\SubmissionAgencyDAO; +use PKP\submission\SubmissionAgencyVocab; use PKP\submission\SubmissionDisciplineDAO; use PKP\submission\SubmissionKeywordDAO; use PKP\submission\SubmissionSubjectDAO; @@ -126,7 +126,7 @@ public function getMany(Request $illuminateRequest): JsonResponse $submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var \PKP\submission\SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */ $entries = $submissionDisciplineEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); break; - case SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY: + case SubmissionAgencyVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY: $submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var \PKP\submission\SubmissionAgencyEntryDAO $submissionAgencyEntryDao */ $entries = $submissionAgencyEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); break; diff --git a/classes/components/forms/publication/PKPMetadataForm.php b/classes/components/forms/publication/PKPMetadataForm.php index b5929b797d9..ae65c61ce6b 100644 --- a/classes/components/forms/publication/PKPMetadataForm.php +++ b/classes/components/forms/publication/PKPMetadataForm.php @@ -21,7 +21,7 @@ use PKP\components\forms\FieldText; use PKP\components\forms\FormComponent; use PKP\context\Context; -use PKP\submission\SubmissionAgencyDAO; +use PKP\submission\SubmissionAgencyVocab; use PKP\submission\SubmissionDisciplineDAO; use PKP\submission\SubmissionKeywordDAO; use PKP\submission\SubmissionSubjectDAO; @@ -88,7 +88,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('submission.supportingAgencies'), 'tooltip' => __('manager.setup.metadata.agencies.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', SubmissionAgencyVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('supportingAgencies'), ])); diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index b481d614e56..c1afdf7b9e0 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -6,103 +6,130 @@ /** * @file classes/controlledVocab/ControlledVocab.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * 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 ControlledVocab * - * @ingroup controlled_vocab - * - * @see ControlledVocabDAO - * - * @brief Basic class describing an controlled vocab. + * @brief */ namespace PKP\controlledVocab; -use PKP\db\DAORegistry; +use Eloquence\Behaviours\HasCamelCasing; +use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Facades\DB; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Builder; +use PKP\facades\Locale; -class ControlledVocab extends \PKP\core\DataObject +class ControlledVocab extends Model { - // - // Get/set methods - // + use HasCamelCasing; - /** - * get assoc id - * - * @return int - */ - public function getAssocId() + protected $table = 'controlled_vocabs'; + protected $primaryKey = 'controlled_vocab_id'; + + protected $guarded = [ + 'controlled_vocab_id', + ]; + + protected function casts(): array { - return $this->getData('assocId'); + return [ + 'symbolic' => 'string', + 'assoc_type' => 'integer', + 'assoc_id' => 'integer', + ]; } /** - * set assoc id - * - * @param int $assocId + * Accessor and Mutator for primary key => id */ - public function setAssocId($assocId) + protected function id(): Attribute { - $this->setData('assocId', $assocId); + return Attribute::make( + get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn($value) => [$this->primaryKey => $value], + ); } /** - * Get associated type. + * Compatibility function for including note IDs in grids. * - * @return int + * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. */ - public function getAssocType() + public function getId(): int { - return $this->getData('assocType'); + return $this->id; } /** - * Set associated type. + * Compatibility function for including notes in grids. * - * @param int $assocType + * @deprecated 3.5. Use $model or $model->$field instead. Can be removed once the DataObject pattern is removed. */ - public function setAssocType($assocType) + public function getData(?string $field): mixed { - $this->setData('assocType', $assocType); + return $field ? $this->$field : $this; } /** - * Get symbolic name. - * - * @return string + * Scope a query to only include notes with a specific user ID. */ - public function getSymbolic() + public function scopeWithSymbolic(Builder $query, string $symbolic): Builder { - return $this->getData('symbolic'); + return $query->where(DB::raw('lower(symbolic)'), strtolower($symbolic)); } /** - * Set symbolic name. - * - * @param string $symbolic + * Scope a query to only include notes with a specific assoc type and assoc ID. */ - public function setSymbolic($symbolic) + public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Builder { - $this->setData('symbolic', $symbolic); + return $query + ->where('assoc_type', $assocType) + ->where('assoc_id', $assocId); } /** * Get a list of controlled vocabulary options. * * @param string $settingName optional - * * @return array $controlledVocabEntryId => name */ - public function enumerate($settingName = 'name') - { - $controlledVocabDao = DAORegistry::getDAO('ControlledVocabDAO'); /** @var ControlledVocabDAO $controlledVocabDao */ - return $controlledVocabDao->enumerate($this->getId(), $settingName); + public function enumerate(string $settingName = 'name'): array + { + return DB::table('controlled_vocab_entries AS e') + ->leftJoin('controlled_vocab_entry_settings AS l', fn (JoinClause $join) => $join + ->on('l.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') + ->where('l.setting_name', '=', $settingName) + ->where('l.locale', '=', Locale::getLocale()) + ) + ->leftJoin('controlled_vocab_entry_settings AS p', fn (JoinClause $join) => $join + ->on('p.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') + ->where('p.setting_name', '=', $settingName) + ->where('p.locale', '=', Locale::getPrimaryLocale()) + ) + ->leftJoin('controlled_vocab_entry_settings AS n', fn (JoinClause $join) => $join + ->on('n.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') + ->where('n.setting_name', '=', $settingName) + ->where('n.locale', '=', '') + ) + ->select([ + 'e.controlled_vocab_entry_id', + DB::raw( + 'coalesce (l.setting_value, p.setting_value, n.setting_value) as setting_value' + ), + DB::raw( + 'coalesce (l.setting_type, p.setting_type, n.setting_type) as setting_type' + ), + ]) + ->where('e.controlled_vocab_id', '=', $this->id) + ->get() + ->pluck('setting_value', 'controlled_vocab_entry_id') + ->toArray(); } } - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\controlledVocab\ControlledVocab', '\ControlledVocab'); -} diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php new file mode 100644 index 00000000000..cc8c5ab07bf --- /dev/null +++ b/classes/controlledVocab/Repository.php @@ -0,0 +1,127 @@ +withAssoc($assocType, $assocId) + ->firstOr(fn() => ControlledVocab::create([ + 'assocType' => $assocType, + 'assocId' => $assocId, + 'symbolic' => $symbolic, + ])); + } + + /** + * Return the Controlled Vocab Entry DAO for this Controlled Vocab. + * Can be subclassed to provide extended DAOs. + * + * Will be removed once the eloquent based settings table relations task completes. + */ + public function getEntryDAO(): ControlledVocabEntryDAO + { + return DAORegistry::getDAO('ControlledVocabEntryDAO'); + } + + public function getEntryDaoBySymbolic(string $symbolic): ControlledVocabEntryDAO + { + return DAORegistry::getDAO(ucfirst($symbolic) . 'EntryDAO'); + } + + public function getBySymbolic( + string $symbolic, + int $assocType, + int $assocId, + array $locales = [], + ?string $entryDaoClassName = null): array + { + $result = []; + + $controlledVocab = $this->build($symbolic, $assocType, $assocId); + + /** @var ControlledVocabEntryDAO $entryDao */ + $entryDao = $entryDaoClassName + ? DAORegistry::getDAO($entryDaoClassName) + : $this->getEntryDaoBySymbolic($symbolic); + + $controlledVocabEntries = $entryDao->getByControlledVocabId($controlledVocab->id); + + while ($vocabEntry = $controlledVocabEntries->next()) { + $vocabs = $vocabEntry->getData($symbolic); + foreach ($vocabs as $locale => $value) { + if (empty($locales) || in_array($locale, $locales)) { + $result[$locale][] = $value; + } + } + } + + return $result; + } + + /** + * Get an array of all of the vocabs for given symbolic + */ + public function getAllUniqueBySymbolic(string $symbolic): array + { + return DB::table('controlled_vocab_entry_settings') + ->select('setting_value') + ->where('setting_name', $symbolic) + ->distinct() + ->get() + ->pluck('setting_value') + ->toArray(); + } + + /** + * Add an array of vocabs + */ + public function insertBySymbolic( + string $symbolic, + array $vocabs, + int $assocType, + int $assocId, + bool $deleteFirst = true, + ?string $entryDaoClassName = null): void + { + /** @var ControlledVocabEntryDAO $entryDao */ + $entryDao = $entryDaoClassName + ? DAORegistry::getDAO($entryDaoClassName) + : $this->getEntryDaoBySymbolic($symbolic); + + $currentControlledVocab = $this->build($symbolic, $assocType, $assocId); + + if ($deleteFirst) { + collect($currentControlledVocab->enumerate( $symbolic)) + ->keys() + ->each(fn (int $id) => $entryDao->deleteObjectById($id)); + } + + if (is_array($vocabs)) { // localized, array of arrays + foreach ($vocabs as $locale => $list) { + if (is_array($list)) { + $list = array_unique($list); // Remove any duplicate keywords + $i = 1; + foreach ($list as $vocab) { + $vocabEntry = $entryDao->newDataObject(); + $vocabEntry->setControlledVocabId($currentControlledVocab->id); + $vocabEntry->setData($symbolic, $vocab, $locale); + $vocabEntry->setSequence($i); + $i++; + $entryDao->insertObject($vocabEntry); + } + } + } + } + } +} diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index b6d3c1f6974..8dfd22124a3 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -464,7 +464,6 @@ public function getDAOMap(): array return [ 'AnnouncementTypeDAO' => 'PKP\announcement\AnnouncementTypeDAO', 'CitationDAO' => 'PKP\citation\CitationDAO', - 'ControlledVocabDAO' => 'PKP\controlledVocab\ControlledVocabDAO', 'ControlledVocabEntryDAO' => 'PKP\controlledVocab\ControlledVocabEntryDAO', 'DataObjectTombstoneDAO' => 'PKP\tombstone\DataObjectTombstoneDAO', 'DataObjectTombstoneSettingsDAO' => 'PKP\tombstone\DataObjectTombstoneSettingsDAO', @@ -490,7 +489,6 @@ public function getDAOMap(): array 'RoleDAO' => 'PKP\security\RoleDAO', 'SiteDAO' => 'PKP\site\SiteDAO', 'SubEditorsDAO' => 'PKP\context\SubEditorsDAO', - 'SubmissionAgencyDAO' => 'PKP\submission\SubmissionAgencyDAO', 'SubmissionAgencyEntryDAO' => 'PKP\submission\SubmissionAgencyEntryDAO', 'SubmissionCommentDAO' => 'PKP\submission\SubmissionCommentDAO', 'SubmissionDisciplineDAO' => 'PKP\submission\SubmissionDisciplineDAO', diff --git a/classes/facades/Repo.php b/classes/facades/Repo.php index 70ae1392cb7..8a4df6f8f4a 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -27,6 +27,7 @@ use PKP\announcement\Repository as AnnouncementRepository; use PKP\author\Repository as AuthorRepository; use PKP\category\Repository as CategoryRepository; +use PKP\controlledVocab\Repository as ControlledVocabRepository; use PKP\decision\Repository as DecisionRepository; use PKP\emailTemplate\Repository as EmailTemplateRepository; use PKP\highlight\Repository as HighlightRepository; @@ -140,4 +141,9 @@ public static function query(): QueryRepository { return app(QueryRepository::class); } + + public static function controlledVocab(): ControlledVocabRepository + { + return app(ControlledVocabRepository::class); + } } diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index fb23666ebe7..9ac27e1189f 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -23,7 +23,7 @@ use PKP\core\EntityDAO; use PKP\core\traits\EntityWithParent; use PKP\services\PKPSchemaService; -use PKP\submission\SubmissionAgencyDAO; +use PKP\submission\SubmissionAgencyVocab; use PKP\submission\SubmissionDisciplineDAO; use PKP\submission\SubmissionKeywordDAO; use PKP\submission\SubmissionSubjectDAO; @@ -58,9 +58,6 @@ class DAO extends EntityDAO /** @var SubmissionDisciplineDAO */ public $submissionDisciplineDao; - /** @var SubmissionAgencyDAO */ - public $submissionAgencyDao; - /** @var CitationDAO */ public $citationDao; @@ -71,7 +68,6 @@ public function __construct( SubmissionKeywordDAO $submissionKeywordDao, SubmissionSubjectDAO $submissionSubjectDao, SubmissionDisciplineDAO $submissionDisciplineDao, - SubmissionAgencyDAO $submissionAgencyDao, CitationDAO $citationDao, PKPSchemaService $schemaService ) { @@ -80,7 +76,6 @@ public function __construct( $this->submissionKeywordDao = $submissionKeywordDao; $this->submissionSubjectDao = $submissionSubjectDao; $this->submissionDisciplineDao = $submissionDisciplineDao; - $this->submissionAgencyDao = $submissionAgencyDao; $this->citationDao = $citationDao; } @@ -373,7 +368,7 @@ protected function setControlledVocab(Publication $publication) $publication->setData('keywords', $this->submissionKeywordDao->getKeywords($publication->getId())); $publication->setData('subjects', $this->submissionSubjectDao->getSubjects($publication->getId())); $publication->setData('disciplines', $this->submissionDisciplineDao->getDisciplines($publication->getId())); - $publication->setData('supportingAgencies', $this->submissionAgencyDao->getAgencies($publication->getId())); + $publication->setData('supportingAgencies', SubmissionAgencyVocab::getAgencies($publication->getId())); } /** @@ -421,7 +416,7 @@ protected function saveControlledVocab(array $values, int $publicationId) $this->submissionDisciplineDao->insertDisciplines($value, $publicationId); break; case 'supportingAgencies': - $this->submissionAgencyDao->insertAgencies($value, $publicationId); + SubmissionAgencyVocab::insertAgencies($value, $publicationId); break; } } @@ -435,7 +430,7 @@ protected function deleteControlledVocab(int $publicationId) $this->submissionKeywordDao->insertKeywords([], $publicationId); $this->submissionSubjectDao->insertSubjects([], $publicationId); $this->submissionDisciplineDao->insertDisciplines([], $publicationId); - $this->submissionAgencyDao->insertAgencies([], $publicationId); + SubmissionAgencyVocab::insertAgencies([], $publicationId); } /** diff --git a/classes/submission/SubmissionAgencyDAO.php b/classes/submission/SubmissionAgencyDAO.php deleted file mode 100644 index 44e6861bf8d..00000000000 --- a/classes/submission/SubmissionAgencyDAO.php +++ /dev/null @@ -1,139 +0,0 @@ -build($publicationId, $assocType); - $submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var SubmissionAgencyEntryDAO $submissionAgencyEntryDao */ - $submissionAgencies = $submissionAgencyEntryDao->getByControlledVocabId($agencies->getId()); - while ($agencyEntry = $submissionAgencies->next()) { - $agency = $agencyEntry->getAgency(); - foreach ($agency as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { - $result[$locale][] = $value; - } - } - } - - return $result; - } - - /** - * Get an array of all of the submission's agencies - * - * @return array - */ - public function getAllUniqueAgencies() - { - $result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_AGENCY]); - - $agencies = []; - foreach ($result as $row) { - $agencies[] = $row->setting_value; - } - return $agencies; - } - - /** - * Add an array of agencies - * - * @param array $agencies List of agencies. - * @param int $publicationId Submission ID. - * @param bool $deleteFirst True iff existing agencies should be removed first. - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function insertAgencies($agencies, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION) - { - $agencyDao = DAORegistry::getDAO('SubmissionAgencyDAO'); /** @var SubmissionAgencyDAO $agencyDao */ - $submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var SubmissionAgencyEntryDAO $submissionAgencyEntryDao */ - $currentAgencies = $this->build($publicationId, $assocType); - - if ($deleteFirst) { - $existingEntries = $agencyDao->enumerate($currentAgencies->getId(), self::CONTROLLED_VOCAB_SUBMISSION_AGENCY); - - foreach ($existingEntries as $id => $entry) { - $entry = trim($entry); - $submissionAgencyEntryDao->deleteObjectById($id); - } - } - if (is_array($agencies)) { // localized, array of arrays - foreach ($agencies as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate keywords - $i = 1; - foreach ($list as $agency) { - $agencyEntry = $submissionAgencyEntryDao->newDataObject(); - $agencyEntry->setControlledVocabId($currentAgencies->getId()); - $agencyEntry->setAgency($agency, $locale); - $agencyEntry->setSequence($i); - $i++; - $submissionAgencyEntryDao->insertObject($agencyEntry); - } - } - } - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionAgencyDAO', '\SubmissionAgencyDAO'); - define('CONTROLLED_VOCAB_SUBMISSION_AGENCY', SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY); -} diff --git a/classes/submission/SubmissionAgencyVocab.php b/classes/submission/SubmissionAgencyVocab.php new file mode 100644 index 00000000000..8a83d54ef63 --- /dev/null +++ b/classes/submission/SubmissionAgencyVocab.php @@ -0,0 +1,78 @@ +getBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + $assocType, + $publicationId, + $locales + ); + } + + /** + * Get an array of all of the submission's agencies + */ + public function scoprGetAllUniqueAgencies():array + { + return Repo::controlledVocab()->getAllUniqueBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_AGENCY + ); + } + + /** + * Add an array of agencies + * + * @param array $agencies List of agencies. + * @param int $publicationId Submission ID. + * @param bool $deleteFirst True iff existing agencies should be removed first. + * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 + */ + public function scoprInsertAgencies(array $agencies, int $publicationId, bool $deleteFirst = true, int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION): void + { + Repo::controlledVocab()->insertBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + $agencies, + $assocType, + $publicationId, + $deleteFirst + ); + } +} diff --git a/classes/validation/ValidatorControlledVocab.php b/classes/validation/ValidatorControlledVocab.php index 935d486e3f0..93600854a53 100644 --- a/classes/validation/ValidatorControlledVocab.php +++ b/classes/validation/ValidatorControlledVocab.php @@ -17,45 +17,35 @@ namespace PKP\validation; -use PKP\controlledVocab\ControlledVocabDAO; -use PKP\db\DAORegistry; +use PKP\controlledVocab\ControlledVocab; class ValidatorControlledVocab extends Validator { - /** @var array */ - public $_acceptedValues; + public array $acceptedValues; /** - * Constructor. - * - * @param string $symbolic - * @param int $assocType - * @param int $assocId + * Constructor */ - public function __construct($symbolic, $assocType, $assocId) + public function __construct(string $symbolic, int $assocType, int $assocId) { - $controlledVocabDao = DAORegistry::getDAO('ControlledVocabDAO'); /** @var ControlledVocabDAO $controlledVocabDao */ - $controlledVocab = $controlledVocabDao->getBySymbolic($symbolic, $assocType, $assocId); - if ($controlledVocab) { - $this->_acceptedValues = array_keys($controlledVocab->enumerate()); - } else { - $this->_acceptedValues = []; - } + $controlledVocab = ControlledVocab::withSymbolic($symbolic) + ->withAssoc($assocType, $assocId) + ->first(); + + $this->acceptedValues = $controlledVocab?->enumerate() ?? []; } - // // Implement abstract methods from Validator // + /** - * @see Validator::isValid() * Value is valid if it is empty and optional or is in the set of accepted values. - * - * @return bool + * @see Validator::isValid() */ - public function isValid($value) + public function isValid($value): bool { - return in_array($value, $this->_acceptedValues); + return in_array($value, $this->acceptedValues); } } diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index 0149f9fab33..dc0af8d14dc 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -138,7 +138,7 @@ public function handleChildElement($n, $publication) if (in_array($n->tagName, $setterMappings)) { $publication->setData($n->tagName, $value, $locale); } elseif (isset($controlledVocabulariesMappings[$n->tagName])) { - $controlledVocabulariesDao = $submissionKeywordDao = DAORegistry::getDAO($controlledVocabulariesMappings[$n->tagName][0]); + $controlledVocabulariesModel = $submissionKeywordModel = $controlledVocabulariesMappings[$n->tagName][0]; $insertFunction = $controlledVocabulariesMappings[$n->tagName][1]; $controlledVocabulary = []; @@ -151,7 +151,7 @@ public function handleChildElement($n, $publication) $controlledVocabulariesValues = []; $controlledVocabulariesValues[$locale] = $controlledVocabulary; - $controlledVocabulariesDao->$insertFunction($controlledVocabulariesValues, $publication->getId(), false); + $controlledVocabulariesModel::$insertFunction($controlledVocabulariesValues, $publication->getId(), false); $publicationNew = Repo::publication()->get($publication->getId()); $publication->setData($n->tagName, $publicationNew->getData($n->tagName)); @@ -316,7 +316,7 @@ public function _getControlledVocabulariesMappings() { return [ 'keywords' => ['SubmissionKeywordDAO', 'insertKeywords'], - 'agencies' => ['SubmissionAgencyDAO', 'insertAgencies'], + 'agencies' => ['SubmissionAgencyVocab', 'insertAgencies'], 'disciplines' => ['SubmissionDisciplineDAO', 'insertDisciplines'], 'subjects' => ['SubmissionSubjectDAO', 'insertSubjects'], ]; diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index 30a902e67d6..ced091e7c89 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -216,10 +216,10 @@ public function addMetadata($doc, $entityNode, $entity) $supportedLocales = $deployment->getContext()->getSupportedFormLocales(); $controlledVocabulariesMapping = $this->_getControlledVocabulariesMappings(); foreach ($controlledVocabulariesMapping as $controlledVocabulariesNodeName => $mappings) { - $dao = DAORegistry::getDAO($mappings[0]); + $vocabModel = $mappings[0]; $getFunction = $mappings[1]; $controlledVocabularyNodeName = $mappings[2]; - $controlledVocabulary = $dao->$getFunction($entity->getId(), $supportedLocales); + $controlledVocabulary = $vocabModel::$getFunction($entity->getId(), $supportedLocales); $this->addControlledVocabulary($doc, $entityNode, $controlledVocabulariesNodeName, $controlledVocabularyNodeName, $controlledVocabulary); } } @@ -305,7 +305,7 @@ public function _getControlledVocabulariesMappings() { return [ 'keywords' => ['SubmissionKeywordDAO', 'getKeywords', 'keyword'], - 'agencies' => ['SubmissionAgencyDAO', 'getAgencies', 'agency'], + 'agencies' => ['SubmissionAgencyVocab', 'getAgencies', 'agency'], 'disciplines' => ['SubmissionDisciplineDAO', 'getDisciplines', 'discipline'], 'subjects' => ['SubmissionSubjectDAO', 'getSubjects', 'subject'], ]; diff --git a/tests/classes/publication/PublicationTest.php b/tests/classes/publication/PublicationTest.php index 5e57895f00d..d6f329f5f58 100644 --- a/tests/classes/publication/PublicationTest.php +++ b/tests/classes/publication/PublicationTest.php @@ -21,7 +21,7 @@ use APP\publication\Publication; use PKP\citation\CitationDAO; use PKP\services\PKPSchemaService; -use PKP\submission\SubmissionAgencyDAO; +use PKP\submission\SubmissionAgencyVocab; use PKP\submission\SubmissionDisciplineDAO; use PKP\submission\SubmissionKeywordDAO; use PKP\submission\SubmissionSubjectDAO; @@ -43,7 +43,6 @@ protected function setUp(): void new SubmissionKeywordDAO(), new SubmissionSubjectDAO(), new SubmissionDisciplineDAO(), - new SubmissionAgencyDAO(), new CitationDAO(), new PKPSchemaService() ))->newDataObject(); From 017ace38dcbb922304711cb08aa6db3b6e6ce529 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 19 Aug 2024 17:09:15 +0600 Subject: [PATCH 02/30] pkp/pkp-lib#10292 WIP: Controlled Vocab DAO to Eloquent Model --- api/v1/vocabs/PKPVocabController.php | 12 +- .../components/forms/publication/Details.php | 4 +- .../forms/publication/PKPMetadataForm.php | 12 +- classes/controlledVocab/ControlledVocab.php | 42 +++-- classes/controlledVocab/Repository.php | 57 ++++++- classes/core/PKPApplication.php | 3 - classes/publication/DAO.php | 59 ++----- classes/submission/SubmissionAgencyVocab.php | 20 ++- .../submission/SubmissionDisciplineDAO.php | 140 --------------- .../submission/SubmissionDisciplineVocab.php | 92 ++++++++++ classes/submission/SubmissionKeywordDAO.php | 161 ------------------ classes/submission/SubmissionKeywordVocab.php | 94 ++++++++++ classes/submission/SubmissionLanguageDAO.php | 141 --------------- classes/submission/SubmissionSubjectDAO.php | 141 --------------- classes/submission/SubmissionSubjectVocab.php | 90 ++++++++++ classes/user/InterestEntry.php | 4 - classes/user/InterestManager.php | 53 ++---- classes/user/UserInterest.php | 161 ++++++++++++++++++ classes/user/maps/Schema.php | 6 +- .../filter/NativeXmlPKPPublicationFilter.php | 6 +- .../filter/PKPPublicationNativeXmlFilter.php | 6 +- .../FormValidatorControlledVocabTest.php | 12 +- tests/classes/publication/PublicationTest.php | 9 +- 23 files changed, 606 insertions(+), 719 deletions(-) delete mode 100644 classes/submission/SubmissionDisciplineDAO.php create mode 100644 classes/submission/SubmissionDisciplineVocab.php delete mode 100644 classes/submission/SubmissionKeywordDAO.php create mode 100644 classes/submission/SubmissionKeywordVocab.php delete mode 100644 classes/submission/SubmissionLanguageDAO.php delete mode 100644 classes/submission/SubmissionSubjectDAO.php create mode 100644 classes/submission/SubmissionSubjectVocab.php create mode 100644 classes/user/UserInterest.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 133f2addb32..55dda0f32f3 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -32,9 +32,9 @@ use PKP\security\authorization\UserRolesRequiredPolicy; use PKP\security\Role; use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineDAO; -use PKP\submission\SubmissionKeywordDAO; -use PKP\submission\SubmissionSubjectDAO; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionSubjectVocab; class PKPVocabController extends PKPBaseController { @@ -114,15 +114,15 @@ public function getMany(Request $illuminateRequest): JsonResponse } switch ($vocab) { - case SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD: + case SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD: $submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var \PKP\submission\SubmissionKeywordEntryDAO $submissionKeywordEntryDao */ $entries = $submissionKeywordEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); break; - case SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT: + case SubmissionSubjectVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT: $submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var \PKP\submission\SubmissionSubjectEntryDAO $submissionSubjectEntryDao */ $entries = $submissionSubjectEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); break; - case SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE: + case SubmissionDisciplineVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE: $submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var \PKP\submission\SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */ $entries = $submissionDisciplineEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); break; diff --git a/classes/components/forms/publication/Details.php b/classes/components/forms/publication/Details.php index 67f56471329..b03b3d7bed8 100644 --- a/classes/components/forms/publication/Details.php +++ b/classes/components/forms/publication/Details.php @@ -18,7 +18,7 @@ use APP\publication\Publication; use PKP\components\forms\FieldControlledVocab; use PKP\context\Context; -use PKP\submission\SubmissionKeywordDAO; +use PKP\submission\SubmissionKeywordVocab; class Details extends TitleAbstractForm { @@ -46,7 +46,7 @@ public function __construct( 'label' => __('common.keywords'), 'description' => __('manager.setup.metadata.keywords.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('keywords'), 'isRequired' => $context->getData('keywords') === Context::METADATA_REQUIRE ? true : false, diff --git a/classes/components/forms/publication/PKPMetadataForm.php b/classes/components/forms/publication/PKPMetadataForm.php index ae65c61ce6b..c7dd0f8ade9 100644 --- a/classes/components/forms/publication/PKPMetadataForm.php +++ b/classes/components/forms/publication/PKPMetadataForm.php @@ -22,9 +22,9 @@ use PKP\components\forms\FormComponent; use PKP\context\Context; use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineDAO; -use PKP\submission\SubmissionKeywordDAO; -use PKP\submission\SubmissionSubjectDAO; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionSubjectVocab; class PKPMetadataForm extends FormComponent { @@ -55,7 +55,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('common.keywords'), 'tooltip' => __('manager.setup.metadata.keywords.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('keywords'), ])); @@ -66,7 +66,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('common.subjects'), 'tooltip' => __('manager.setup.metadata.subjects.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', SubmissionSubjectVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('subjects'), ])); @@ -77,7 +77,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('search.discipline'), 'tooltip' => __('manager.setup.metadata.disciplines.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', SubmissionDisciplineVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('disciplines'), ])); diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index c1afdf7b9e0..485541a2e8a 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -36,6 +36,13 @@ class ControlledVocab extends Model 'controlled_vocab_id', ]; + /** + * Indicates if the model should be timestamped. + * + * @var bool + */ + public $timestamps = false; + protected function casts(): array { return [ @@ -81,7 +88,7 @@ public function getData(?string $field): mixed */ public function scopeWithSymbolic(Builder $query, string $symbolic): Builder { - return $query->where(DB::raw('lower(symbolic)'), strtolower($symbolic)); + return $query->where(DB::raw('LOWER(symbolic)'), strtolower($symbolic)); } /** @@ -103,31 +110,38 @@ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Bu public function enumerate(string $settingName = 'name'): array { return DB::table('controlled_vocab_entries AS e') - ->leftJoin('controlled_vocab_entry_settings AS l', fn (JoinClause $join) => $join - ->on('l.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') - ->where('l.setting_name', '=', $settingName) - ->where('l.locale', '=', Locale::getLocale()) + ->leftJoin( + 'controlled_vocab_entry_settings AS l', + fn (JoinClause $join) => $join + ->on('l.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') + ->where('l.setting_name', $settingName) + ->where('l.locale', Locale::getLocale()) ) - ->leftJoin('controlled_vocab_entry_settings AS p', fn (JoinClause $join) => $join + ->leftJoin( + 'controlled_vocab_entry_settings AS p', + fn (JoinClause $join) => $join ->on('p.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') - ->where('p.setting_name', '=', $settingName) - ->where('p.locale', '=', Locale::getPrimaryLocale()) + ->where('p.setting_name', $settingName) + ->where('p.locale', Locale::getPrimaryLocale()) ) - ->leftJoin('controlled_vocab_entry_settings AS n', fn (JoinClause $join) => $join + ->leftJoin( + 'controlled_vocab_entry_settings AS n', + fn (JoinClause $join) => $join ->on('n.controlled_vocab_entry_id', '=', 'e.controlled_vocab_entry_id') - ->where('n.setting_name', '=', $settingName) - ->where('n.locale', '=', '') + ->where('n.setting_name', $settingName) + ->where('n.locale', '') ) ->select([ 'e.controlled_vocab_entry_id', DB::raw( - 'coalesce (l.setting_value, p.setting_value, n.setting_value) as setting_value' + 'COALESCE (l.setting_value, p.setting_value, n.setting_value) as setting_value' ), DB::raw( - 'coalesce (l.setting_type, p.setting_type, n.setting_type) as setting_type' + 'COALESCE (l.setting_type, p.setting_type, n.setting_type) as setting_type' ), ]) - ->where('e.controlled_vocab_id', '=', $this->id) + ->where('e.controlled_vocab_id', $this->id) + ->orderBy('e.seq') ->get() ->pluck('setting_value', 'controlled_vocab_entry_id') ->toArray(); diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index cc8c5ab07bf..43f15ad513a 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -2,8 +2,12 @@ namespace PKP\controlledVocab; -use PKP\db\DAORegistry; +use APP\facades\Repo; use Illuminate\Support\Facades\DB; +use PKP\db\DAORegistry; +use PKP\user\UserInterest; +use PKP\user\InterestEntry; +use PKP\user\InterestEntryDAO; use PKP\controlledVocab\ControlledVocab; use PKP\controlledVocab\ControlledVocabEntryDAO; @@ -44,7 +48,8 @@ public function getBySymbolic( int $assocType, int $assocId, array $locales = [], - ?string $entryDaoClassName = null): array + ?string $entryDaoClassName = null + ): array { $result = []; @@ -92,7 +97,8 @@ public function insertBySymbolic( int $assocType, int $assocId, bool $deleteFirst = true, - ?string $entryDaoClassName = null): void + ?string $entryDaoClassName = null + ): void { /** @var ControlledVocabEntryDAO $entryDao */ $entryDao = $entryDaoClassName @@ -124,4 +130,49 @@ public function insertBySymbolic( } } } + + /** + * Update a user's set of interests + */ + public function setUserInterests(array $interests, int $userId): void + { + $controlledVocab = Repo::controlledVocab()->build( + UserInterest::CONTROLLED_VOCAB_INTEREST + ); + + /** @var InterestEntryDAO $interestEntryDao */ + $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); + + DB::beginTransaction(); + + // Delete the existing interests association. + UserInterest::withUserId($userId)->delete(); + + collect($interests) + ->map(fn (string $interest): string => trim($interest)) + ->unique() + ->each(function (string $interest) use ($controlledVocab, $interestEntryDao, $userId): void { + $interestEntry = $interestEntryDao->getBySetting( + $interest, + $controlledVocab->symbolic, + $controlledVocab->assocId, + $controlledVocab->assocType, + $controlledVocab->symbolic + ); + + if (!$interestEntry) { + $interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */ + $interestEntry->setInterest($interest); + $interestEntry->setControlledVocabId($controlledVocab->id); + $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); + } + + UserInterest::create([ + 'userId' => $userId, + 'controlledVocabEntryId' => $interestEntry->getId(), + ]); + }); + + DB::commit(); + } } diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index 8dfd22124a3..e59da6c8a8e 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -470,7 +470,6 @@ public function getDAOMap(): array 'FilterDAO' => 'PKP\filter\FilterDAO', 'FilterGroupDAO' => 'PKP\filter\FilterGroupDAO', 'GenreDAO' => 'PKP\submission\GenreDAO', - 'InterestDAO' => 'PKP\user\InterestDAO', 'InterestEntryDAO' => 'PKP\user\InterestEntryDAO', 'LibraryFileDAO' => 'PKP\context\LibraryFileDAO', 'NavigationMenuDAO' => 'PKP\navigationMenu\NavigationMenuDAO', @@ -491,11 +490,9 @@ public function getDAOMap(): array 'SubEditorsDAO' => 'PKP\context\SubEditorsDAO', 'SubmissionAgencyEntryDAO' => 'PKP\submission\SubmissionAgencyEntryDAO', 'SubmissionCommentDAO' => 'PKP\submission\SubmissionCommentDAO', - 'SubmissionDisciplineDAO' => 'PKP\submission\SubmissionDisciplineDAO', 'SubmissionDisciplineEntryDAO' => 'PKP\submission\SubmissionDisciplineEntryDAO', 'SubmissionKeywordDAO' => 'PKP\submission\SubmissionKeywordDAO', 'SubmissionKeywordEntryDAO' => 'PKP\submission\SubmissionKeywordEntryDAO', - 'SubmissionSubjectDAO' => 'PKP\submission\SubmissionSubjectDAO', 'SubmissionSubjectEntryDAO' => 'PKP\submission\SubmissionSubjectEntryDAO', 'TemporaryFileDAO' => 'PKP\file\TemporaryFileDAO', 'TemporaryInstitutionsDAO' => 'PKP\statistics\TemporaryInstitutionsDAO', diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 9ac27e1189f..4c6cab99e90 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -24,9 +24,9 @@ use PKP\core\traits\EntityWithParent; use PKP\services\PKPSchemaService; use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineDAO; -use PKP\submission\SubmissionKeywordDAO; -use PKP\submission\SubmissionSubjectDAO; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionSubjectVocab; /** * @template T of Publication @@ -49,33 +49,16 @@ class DAO extends EntityDAO /** @copydoc EntityDAO::$primaryKeyColumn */ public $primaryKeyColumn = 'publication_id'; - /** @var SubmissionKeywordDAO */ - public $submissionKeywordDao; - - /** @var SubmissionSubjectDAO */ - public $submissionSubjectDao; - - /** @var SubmissionDisciplineDAO */ - public $submissionDisciplineDao; - /** @var CitationDAO */ public $citationDao; /** * Constructor */ - public function __construct( - SubmissionKeywordDAO $submissionKeywordDao, - SubmissionSubjectDAO $submissionSubjectDao, - SubmissionDisciplineDAO $submissionDisciplineDao, - CitationDAO $citationDao, - PKPSchemaService $schemaService - ) { + public function __construct(CitationDAO $citationDao, PKPSchemaService $schemaService) + { parent::__construct($schemaService); - $this->submissionKeywordDao = $submissionKeywordDao; - $this->submissionSubjectDao = $submissionSubjectDao; - $this->submissionDisciplineDao = $submissionDisciplineDao; $this->citationDao = $citationDao; } @@ -365,9 +348,9 @@ protected function deleteAuthors(int $publicationId) */ protected function setControlledVocab(Publication $publication) { - $publication->setData('keywords', $this->submissionKeywordDao->getKeywords($publication->getId())); - $publication->setData('subjects', $this->submissionSubjectDao->getSubjects($publication->getId())); - $publication->setData('disciplines', $this->submissionDisciplineDao->getDisciplines($publication->getId())); + $publication->setData('keywords', SubmissionKeywordVocab::getKeywords($publication->getId())); + $publication->setData('subjects', SubmissionSubjectVocab::getSubjects($publication->getId())); + $publication->setData('disciplines', SubmissionDisciplineVocab::getDisciplines($publication->getId())); $publication->setData('supportingAgencies', SubmissionAgencyVocab::getAgencies($publication->getId())); } @@ -405,20 +388,12 @@ protected function saveControlledVocab(array $values, int $publicationId) { // Update controlled vocabularly for which we have props foreach ($values as $prop => $value) { - switch ($prop) { - case 'keywords': - $this->submissionKeywordDao->insertKeywords($value, $publicationId); - break; - case 'subjects': - $this->submissionSubjectDao->insertSubjects($value, $publicationId); - break; - case 'disciplines': - $this->submissionDisciplineDao->insertDisciplines($value, $publicationId); - break; - case 'supportingAgencies': - SubmissionAgencyVocab::insertAgencies($value, $publicationId); - break; - } + match ($prop) { + 'keywords' => SubmissionKeywordVocab::insertKeywords($value, $publicationId), + 'subjects' => SubmissionSubjectVocab::insertSubjects($value, $publicationId), + 'disciplines' => SubmissionDisciplineVocab::insertDisciplines($value, $publicationId), + 'supportingAgencies' => SubmissionAgencyVocab::insertAgencies($value, $publicationId), + }; } } @@ -427,9 +402,9 @@ protected function saveControlledVocab(array $values, int $publicationId) */ protected function deleteControlledVocab(int $publicationId) { - $this->submissionKeywordDao->insertKeywords([], $publicationId); - $this->submissionSubjectDao->insertSubjects([], $publicationId); - $this->submissionDisciplineDao->insertDisciplines([], $publicationId); + SubmissionKeywordVocab::insertKeywords([], $publicationId); + SubmissionSubjectVocab::insertSubjects([], $publicationId); + SubmissionDisciplineVocab::insertDisciplines([], $publicationId); SubmissionAgencyVocab::insertAgencies([], $publicationId); } diff --git a/classes/submission/SubmissionAgencyVocab.php b/classes/submission/SubmissionAgencyVocab.php index 8a83d54ef63..4ba5d1c6a7e 100644 --- a/classes/submission/SubmissionAgencyVocab.php +++ b/classes/submission/SubmissionAgencyVocab.php @@ -15,6 +15,7 @@ namespace PKP\submission; use APP\facades\Repo; +use Illuminate\Database\Eloquent\Builder; use PKP\controlledVocab\ControlledVocab; use PKP\core\PKPApplication; @@ -26,7 +27,7 @@ class SubmissionAgencyVocab extends ControlledVocab /** * Get the list of localized additional fields to store. */ - public function scopeGetLocaleFieldNames(): array + public function scopeGetLocaleFieldNames(Builder $query): array { return ['submissionAgency']; } @@ -37,7 +38,12 @@ public function scopeGetLocaleFieldNames(): array * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213 * */ - public function scopeGetAgencies(int $publicationId, array $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION): array + public function scopeGetAgencies( + Builder $query, + int $publicationId, + array $locales = [], + int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION + ): array { return Repo::controlledVocab()->getBySymbolic( static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, @@ -50,7 +56,7 @@ public function scopeGetAgencies(int $publicationId, array $locales = [], $assoc /** * Get an array of all of the submission's agencies */ - public function scoprGetAllUniqueAgencies():array + public function scoprGetAllUniqueAgencies(Builder $query):array { return Repo::controlledVocab()->getAllUniqueBySymbolic( static::CONTROLLED_VOCAB_SUBMISSION_AGENCY @@ -65,7 +71,13 @@ public function scoprGetAllUniqueAgencies():array * @param bool $deleteFirst True iff existing agencies should be removed first. * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 */ - public function scoprInsertAgencies(array $agencies, int $publicationId, bool $deleteFirst = true, int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION): void + public function scoprInsertAgencies( + Builder $query, + array $agencies, + int $publicationId, + bool $deleteFirst = true, + int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION + ): void { Repo::controlledVocab()->insertBySymbolic( static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, diff --git a/classes/submission/SubmissionDisciplineDAO.php b/classes/submission/SubmissionDisciplineDAO.php deleted file mode 100644 index 58cbc58d48d..00000000000 --- a/classes/submission/SubmissionDisciplineDAO.php +++ /dev/null @@ -1,140 +0,0 @@ -build($publicationId, $assocType); - $submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */ - $submissionDisciplines = $submissionDisciplineEntryDao->getByControlledVocabId($disciplines->getId()); - while ($disciplineEntry = $submissionDisciplines->next()) { - $discipline = $disciplineEntry->getDiscipline(); - foreach ($discipline as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { - $result[$locale][] = $value; - } - } - } - - return $result; - } - - /** - * Get an array of all of the submission's disciplines - * - * @return array - */ - public function getAllUniqueDisciplines() - { - $result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE]); - - $disciplines = []; - foreach ($result as $row) { - $disciplines[] = $row->setting_value; - } - return $disciplines; - } - - /** - * Add an array of disciplines - * - * @param array $disciplines - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function insertDisciplines($disciplines, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION) - { - $disciplineDao = DAORegistry::getDAO('SubmissionDisciplineDAO'); /** @var SubmissionDisciplineDAO $disciplineDao */ - $submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */ - $currentDisciplines = $this->build($publicationId, $assocType); - - if ($deleteFirst) { - $existingEntries = $disciplineDao->enumerate($currentDisciplines->getId(), self::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE); - - foreach ($existingEntries as $id => $entry) { - $entry = trim($entry); - $submissionDisciplineEntryDao->deleteObjectById($id); - } - } - if (is_array($disciplines)) { // localized, array of arrays - foreach ($disciplines as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate keywords - $i = 1; - foreach ($list as $discipline) { - $disciplineEntry = $submissionDisciplineEntryDao->newDataObject(); - $disciplineEntry->setControlledVocabId($currentDisciplines->getId()); - $disciplineEntry->setDiscipline($discipline, $locale); - $disciplineEntry->setSequence($i); - $i++; - $submissionDisciplineEntryDao->insertObject($disciplineEntry); - } - } - } - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionDisciplineDAO', '\SubmissionDisciplineDAO'); - define('CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE', SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE); -} diff --git a/classes/submission/SubmissionDisciplineVocab.php b/classes/submission/SubmissionDisciplineVocab.php new file mode 100644 index 00000000000..42217ff74f0 --- /dev/null +++ b/classes/submission/SubmissionDisciplineVocab.php @@ -0,0 +1,92 @@ +getBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + $assocType, + $publicationId, + $locales + ); + } + + /** + * Get an array of all of the submission's disciplines + */ + public function scopeGetAllUniqueDisciplines(Builder $query): array + { + return Repo::controlledVocab()->getAllUniqueBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE + ); + } + + /** + * Add an array of disciplines + * + * @param array $disciplines + * @param int $publicationId + * @param bool $deleteFirst + * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 + */ + public function scopeInsertDisciplines( + Builder $query, + array $disciplines, + int $publicationId, + bool $deleteFirst = true, + int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION): void + { + Repo::controlledVocab()->insertBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + $disciplines, + $assocType, + $publicationId, + $deleteFirst + ); + } +} diff --git a/classes/submission/SubmissionKeywordDAO.php b/classes/submission/SubmissionKeywordDAO.php deleted file mode 100644 index cfc420ea416..00000000000 --- a/classes/submission/SubmissionKeywordDAO.php +++ /dev/null @@ -1,161 +0,0 @@ -build($publicationId, $assocType); - $submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */ - $submissionKeywords = $submissionKeywordEntryDao->getByControlledVocabId($keywords->getId()); - while ($keywordEntry = $submissionKeywords->next()) { - $keyword = $keywordEntry->getKeyword(); - if ($keyword) { - foreach ($keyword as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { - if (!array_key_exists($locale, $result)) { - $result[$locale] = []; - } - $result[$locale][] = $value; - } - } - } - } - - return $result; - } - - /** - * Get an array of all of the submission's keywords - * - * @return array - */ - public function getAllUniqueKeywords() - { - $result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_KEYWORD]); - - $keywords = []; - foreach ($result as $row) { - $keywords[] = $row->setting_value; - } - return $keywords; - } - - /** - * Add an array of keywords - * - * @param array $keywords - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function insertKeywords($keywords, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION) - { - $submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */ - - if ($deleteFirst) { - $currentKeywords = $this->deleteByPublicationId($publicationId); - } else { - $currentKeywords = $this->build($publicationId, $assocType); - } - if (is_array($keywords)) { // localized, array of arrays - foreach ($keywords as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate keywords - $i = 1; - foreach ($list as $keyword) { - $keywordEntry = $submissionKeywordEntryDao->newDataObject(); - $keywordEntry->setControlledVocabId($currentKeywords->getId()); - $keywordEntry->setKeyword($keyword, $locale); - $keywordEntry->setSequence($i); - $i++; - $submissionKeywordEntryDao->insertObject($keywordEntry); - } - } - } - } - } - - /** - * Delete keywords by publication ID - * - * @return ControlledVocab Controlled Vocab - */ - public function deleteByPublicationId($publicationId) - { - $keywordDao = DAORegistry::getDAO('SubmissionKeywordDAO'); /** @var SubmissionKeywordDAO $keywordDao */ - $submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */ - $currentKeywords = $this->build($publicationId); - - $existingEntries = $keywordDao->enumerate($currentKeywords->getId(), self::CONTROLLED_VOCAB_SUBMISSION_KEYWORD); - foreach ($existingEntries as $id => $entry) { - $entry = trim($entry); - $entryObj = $submissionKeywordEntryDao->getById($id); - $submissionKeywordEntryDao->deleteObjectById($id); - } - - return $currentKeywords; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionKeywordDAO', '\SubmissionKeywordDAO'); - define('CONTROLLED_VOCAB_SUBMISSION_KEYWORD', SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD); -} diff --git a/classes/submission/SubmissionKeywordVocab.php b/classes/submission/SubmissionKeywordVocab.php new file mode 100644 index 00000000000..6a371e9809e --- /dev/null +++ b/classes/submission/SubmissionKeywordVocab.php @@ -0,0 +1,94 @@ +getBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + $assocType, + $publicationId, + $locales + ); + } + + /** + * Get an array of all of the submission's keywords + * + * @return array + */ + public function scopeGetAllUniqueKeywords(Builder $query): array + { + return Repo::controlledVocab()->getAllUniqueBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD + ); + } + + /** + * Add an array of keywords + * + * @param array $keywords + * @param int $publicationId + * @param bool $deleteFirst + * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 + */ + public function scopeInsertKeywords( + Builder $query, + array $keywords, + int $publicationId, + bool $deleteFirst = true, + int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION + ): void + { + Repo::controlledVocab()->insertBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + $keywords, + $assocType, + $publicationId, + $deleteFirst + ); + } +} diff --git a/classes/submission/SubmissionLanguageDAO.php b/classes/submission/SubmissionLanguageDAO.php deleted file mode 100644 index 9f531504c8c..00000000000 --- a/classes/submission/SubmissionLanguageDAO.php +++ /dev/null @@ -1,141 +0,0 @@ -build($publicationId, $assocType); - $submissionLanguageEntryDao = DAORegistry::getDAO('SubmissionLanguageEntryDAO'); /** @var SubmissionLanguageEntryDAO $submissionLanguageEntryDao */ - $submissionLanguages = $submissionLanguageEntryDao->getByControlledVocabId($languages->getId()); - while ($languageEntry = $submissionLanguages->next()) { - $language = $languageEntry->getLanguage(); - foreach ($language as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { - $result[$locale][] = $value; - } - } - } - - return $result; - } - - /** - * Get an array of all of the submission's Languages - * - * @return array - */ - public function getAllUniqueLanguages() - { - $result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE]); - - $languages = []; - foreach ($result as $row) { - $languages[] = $row->setting_value; - } - return $languages; - } - - /** - * Add an array of languages - * - * @param array $languages - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function insertLanguages($languages, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION) - { - $languageDao = DAORegistry::getDAO('SubmissionLanguageDAO'); /** @var SubmissionLanguageDAO $languageDao */ - $submissionLanguageEntryDao = DAORegistry::getDAO('SubmissionLanguageEntryDAO'); /** @var SubmissionLanguageEntryDAO $submissionLanguageEntryDao */ - $currentLanguages = $this->build($publicationId, $assocType); - - if ($deleteFirst) { - $existingEntries = $languageDao->enumerate($currentLanguages->getId(), self::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE); - - foreach ($existingEntries as $id => $entry) { - $entry = trim($entry); - $submissionLanguageEntryDao->deleteObjectById($id); - } - } - if (is_array($languages)) { // localized, array of arrays - foreach ($languages as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate Languages - $i = 1; - foreach ($list as $language) { - $languageEntry = $submissionLanguageEntryDao->newDataObject(); - $languageEntry->setControlledVocabId($currentLanguages->getId()); - $languageEntry->setLanguage($language, $locale); - $languageEntry->setSequence($i); - $i++; - $submissionLanguageEntryDao->insertObject($languageEntry); - } - } - } - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionLanguageDAO', '\SubmissionLanguageDAO'); - define('CONTROLLED_VOCAB_SUBMISSION_LANGUAGE', SubmissionLanguageDAO::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE); -} diff --git a/classes/submission/SubmissionSubjectDAO.php b/classes/submission/SubmissionSubjectDAO.php deleted file mode 100644 index c8ed091b6ab..00000000000 --- a/classes/submission/SubmissionSubjectDAO.php +++ /dev/null @@ -1,141 +0,0 @@ -build($publicationId, $assocType); - $submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var SubmissionSubjectEntryDAO $submissionSubjectEntryDao */ - $submissionSubjects = $submissionSubjectEntryDao->getByControlledVocabId($subjects->getId()); - /** @var SubmissionSubject */ - foreach ($submissionSubjects->toIterator() as $subjectEntry) { - $subject = $subjectEntry->getSubject(); - foreach ($subject as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { - $result[$locale][] = $value; - } - } - } - - return $result; - } - - /** - * Get an array of all of the submission's Subjects - * - * @return array - */ - public function getAllUniqueSubjects() - { - $result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT]); - - $subjects = []; - foreach ($result as $row) { - $subjects[] = $row->setting_value; - } - return $subjects; - } - - /** - * Add an array of subjects - * - * @param array $subjects - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function insertSubjects($subjects, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION) - { - $subjectDao = DAORegistry::getDAO('SubmissionSubjectDAO'); /** @var SubmissionSubjectDAO $subjectDao */ - $submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var SubmissionSubjectEntryDAO $submissionSubjectEntryDao */ - $currentSubjects = $this->build($publicationId, $assocType); - - if ($deleteFirst) { - $existingEntries = $subjectDao->enumerate($currentSubjects->getId(), SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT); - - foreach ($existingEntries as $id => $entry) { - $entry = trim($entry); - $submissionSubjectEntryDao->deleteObjectById($id); - } - } - if (is_array($subjects)) { // localized, array of arrays - foreach ($subjects as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate Subjects - $i = 1; - foreach ($list as $subject) { - $subjectEntry = $submissionSubjectEntryDao->newDataObject(); - $subjectEntry->setControlledVocabId($currentSubjects->getId()); - $subjectEntry->setSubject($subject, $locale); - $subjectEntry->setSequence($i); - $i++; - $submissionSubjectEntryDao->insertObject($subjectEntry); - } - } - } - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionSubjectDAO', '\SubmissionSubjectDAO'); - define('CONTROLLED_VOCAB_SUBMISSION_SUBJECT', SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT); -} diff --git a/classes/submission/SubmissionSubjectVocab.php b/classes/submission/SubmissionSubjectVocab.php new file mode 100644 index 00000000000..63bb8a09469 --- /dev/null +++ b/classes/submission/SubmissionSubjectVocab.php @@ -0,0 +1,90 @@ +getBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + $assocType, + $publicationId, + $locales + ); + } + + /** + * Get an array of all of the submission's Subjects + */ + public function scopeGetAllUniqueSubjects(Builder $query): array + { + return Repo::controlledVocab()->getAllUniqueBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT + ); + } + + /** + * Add an array of subjects + * + * @param array $subjects + * @param int $publicationId + * @param bool $deleteFirst + * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 + */ + public function scopeInsertSubjects( + Builder $query, + array $subjects, + int $publicationId, + bool $deleteFirst = true, + int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION + ): void + { + Repo::controlledVocab()->insertBySymbolic( + static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + $subjects, + $assocType, + $publicationId, + $deleteFirst + ); + } +} diff --git a/classes/user/InterestEntry.php b/classes/user/InterestEntry.php index 29b857902ce..f87ac7c0b2b 100644 --- a/classes/user/InterestEntry.php +++ b/classes/user/InterestEntry.php @@ -9,10 +9,6 @@ * * @class Interest * - * @ingroup user - * - * @see InterestDAO - * * @brief Basic class describing a reviewer interest */ diff --git a/classes/user/InterestManager.php b/classes/user/InterestManager.php index 84559853854..0090c978201 100644 --- a/classes/user/InterestManager.php +++ b/classes/user/InterestManager.php @@ -3,43 +3,30 @@ /** * @file classes/user/InterestManager.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2000-2024 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class InterestManager * - * @ingroup user - * - * @see InterestDAO - * * @brief Handle user interest functions. */ namespace PKP\user; +use APP\facades\Repo; use PKP\db\DAORegistry; +use PKP\user\User; +use PKP\user\UserInterest; class InterestManager { - /** - * Constructor. - */ - public function __construct() - { - } - /** * Get all interests for all users in the system - * - * @param string $filter - * - * @return array */ - public function getAllInterests($filter = null) + public function getAllInterests(?string $filter = null): array { - $interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */ - $interests = $interestDao->getAllInterests($filter); + $interests = UserInterest::getAllInterests($filter); $interestReturner = []; while ($interest = $interests->next()) { @@ -56,14 +43,14 @@ public function getInterestsForUser(User $user): array { static $interestsCache = []; $interests = []; - $interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */ $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ - $controlledVocab = $interestDao->build(); - foreach ($interestDao->getUserInterestIds($user->getId()) as $interestEntryId) { + $controlledVocab = Repo::controlledVocab()->build(UserInterest::CONTROLLED_VOCAB_INTEREST); + + foreach (UserInterest::getUserInterestIds($user->getId()) as $interestEntryId) { /** @var InterestEntry */ $interestEntry = $interestsCache[$interestEntryId] ??= $interestEntryDao->getById( $interestEntryId, - $controlledVocab->getId() + $controlledVocab->id ); if ($interestEntry) { $interests[] = $interestEntry->getInterest(); @@ -75,12 +62,8 @@ public function getInterestsForUser(User $user): array /** * Returns a comma separated string of a user's interests - * - * @param User $user - * - * @return string */ - public function getInterestsString($user) + public function getInterestsString(User $user): string { $interests = $this->getInterestsForUser($user); @@ -89,14 +72,14 @@ public function getInterestsString($user) /** * Set a user's interests - * - * @param User $user */ - public function setInterestsForUser($user, $interests) + public function setInterestsForUser(User $user, string|array|null $interests = null): void { - $interestDao = DAORegistry::getDAO('InterestDAO'); /** @var InterestDAO $interestDao */ - $interests = is_array($interests) ? $interests : (empty($interests) ? null : explode(',', $interests)); - $interestDao->setUserInterests($interests, $user->getId()); + $interests = is_array($interests) + ? $interests + : (empty($interests) ? [] : explode(',', $interests)); + + Repo::controlledVocab()->setUserInterests($interests, $user->getId()); } } diff --git a/classes/user/UserInterest.php b/classes/user/UserInterest.php new file mode 100644 index 00000000000..67d23d01b41 --- /dev/null +++ b/classes/user/UserInterest.php @@ -0,0 +1,161 @@ + 'integer', + 'controlled_vocab_entry_id' => 'integer', + ]; + } + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn($value) => [$this->primaryKey => $value], + ); + } + + /** + * Compatibility function for including note IDs in grids. + * + * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. + */ + public function getId(): int + { + return $this->id; + } + + /** + * Compatibility function for including notes in grids. + * + * @deprecated 3.5. Use $model or $model->$field instead. Can be removed once the DataObject pattern is removed. + */ + public function getData(?string $field): mixed + { + return $field ? $this->$field : $this; + } + + /** + * Scope a query to only include notes with a specific assoc type and assoc ID. + */ + public function scopeWithUserId(Builder $query, int $userId): Builder + { + return $query->where('user_id', $userId); + } + + /** + * Get a list of controlled vocabulary entry IDs (corresponding to interest keywords) + * attributed to a user + */ + public static function getUserInterestIds(int $userId): array + { + $controlledVocab = Repo::controlledVocab()->build( + static::CONTROLLED_VOCAB_INTEREST + ); + + return DB::table('controlled_vocab_entries AS cve') + ->select(['cve.controlled_vocab_entry_id']) + ->join( + 'user_interests AS ui', + fn (JoinClause $join) => $join + ->on('cve.controlled_vocab_entry_id', '=', 'ui.controlled_vocab_entry_id') + ->where('ui.user_id', $userId) + ) + ->where('controlled_vocab_id', $controlledVocab->id) + ->get() + ->pluck('controlled_vocab_entry_id') + ->toArray(); + } + + /** + * Get a list of user IDs attributed to an interest + */ + public static function getUserIdsByInterest(string $interest): array + { + return DB::table('user_interests AS ui') + ->select('ui.user_id') + ->join( + 'controlled_vocab_entry_settings AS cves', + fn (JoinClause $join) => $join + ->on('cves.controlled_vocab_entry_id', '=', 'ui.controlled_vocab_entry_id') + ->where('cves.setting_name', STATIC::CONTROLLED_VOCAB_INTEREST) + ->where(DB::raw('LOWER(cves.setting_value)'), trim(strtolower($interest))) + ) + ->get() + ->pluck('user_id') + ->toArray(); + } + + + /** + * Get all user's interests + */ + public static function getAllInterests(?string $filter = null): object + { + $controlledVocab = Repo::controlledVocab()->build( + static::CONTROLLED_VOCAB_INTEREST + ); + + $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ + $iterator = $interestEntryDao->getByControlledVocabId($controlledVocab->id, null, $filter); + + // Sort by name. + $interests = $iterator->toArray(); + usort($interests, function ($s1, $s2) { + return strcmp($s1->getInterest(), $s2->getInterest()); + }); + + // Turn back into an iterator. + return new ArrayItemIterator($interests); + } +} diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index 40456345df5..e41169fca1c 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -16,6 +16,7 @@ use APP\facades\Repo; use APP\submission\Submission; use Illuminate\Support\Enumerable; +use PKP\user\UserInterest; use PKP\db\DAORegistry; use PKP\plugins\Hook; use PKP\security\Role; @@ -186,13 +187,12 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat case 'interests': $output[$prop] = []; if ($this->context) { - $interestDao = DAORegistry::getDAO('InterestDAO'); /** @var \PKP\user\InterestDAO $interestDao */ - $interestEntryIds = $interestDao->getUserInterestIds($user->getId()); + $interestEntryIds = UserInterest::getUserInterestIds($user->getId()); if (!empty($interestEntryIds)) { $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var \PKP\user\InterestEntryDAO $interestEntryDao */ $results = $interestEntryDao->getByIds($interestEntryIds); $output[$prop] = []; - while ($interest = $results->next()) { + while ($interest = $results->next()) { /** @var \PKP\user\InterestEntry $interest */ $output[$prop][] = [ 'id' => (int) $interest->getId(), 'interest' => $interest->getInterest(), diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index dc0af8d14dc..ec753b9a9af 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -315,10 +315,10 @@ public function _getLocalizedPublicationFields() public function _getControlledVocabulariesMappings() { return [ - 'keywords' => ['SubmissionKeywordDAO', 'insertKeywords'], + 'keywords' => ['SubmissionKeywordVocab', 'insertKeywords'], 'agencies' => ['SubmissionAgencyVocab', 'insertAgencies'], - 'disciplines' => ['SubmissionDisciplineDAO', 'insertDisciplines'], - 'subjects' => ['SubmissionSubjectDAO', 'insertSubjects'], + 'disciplines' => ['SubmissionDisciplineVocab', 'insertDisciplines'], + 'subjects' => ['SubmissionSubjectVocab', 'insertSubjects'], ]; } diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index ced091e7c89..9e5a37106cb 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -304,10 +304,10 @@ public function addRepresentations($doc, $entityNode, $entity) public function _getControlledVocabulariesMappings() { return [ - 'keywords' => ['SubmissionKeywordDAO', 'getKeywords', 'keyword'], + 'keywords' => ['SubmissionKeywordVocab', 'getKeywords', 'keyword'], 'agencies' => ['SubmissionAgencyVocab', 'getAgencies', 'agency'], - 'disciplines' => ['SubmissionDisciplineDAO', 'getDisciplines', 'discipline'], - 'subjects' => ['SubmissionSubjectDAO', 'getSubjects', 'subject'], + 'disciplines' => ['SubmissionDisciplineVocab', 'getDisciplines', 'discipline'], + 'subjects' => ['SubmissionSubjectVocab', 'getSubjects', 'subject'], ]; } diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index f93ac5a062e..6d88820df87 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -37,7 +37,7 @@ class FormValidatorControlledVocabTest extends PKPTestCase */ protected function getMockedDAOs(): array { - return [...parent::getMockedDAOs(), 'ControlledVocabDAO']; + return [...parent::getMockedDAOs()]; } public function testIsValid() @@ -74,7 +74,15 @@ public function testIsValid() DAORegistry::registerDAO('ControlledVocabDAO', $mockControlledVocabDao); // Instantiate validator - $validator = new \PKP\form\validation\FormValidatorControlledVocab($form, 'testData', FormValidator::FORM_VALIDATOR_REQUIRED_VALUE, 'some.message.key', 'testVocab', Application::ASSOC_TYPE_CITATION, 333); + $validator = new FormValidatorControlledVocab( + $form, + 'testData', + FormValidator::FORM_VALIDATOR_REQUIRED_VALUE, + 'some.message.key', + 'testVocab', + Application::ASSOC_TYPE_CITATION, + 333 + ); $form->setData('testData', '1'); self::assertTrue($validator->isValid()); diff --git a/tests/classes/publication/PublicationTest.php b/tests/classes/publication/PublicationTest.php index d6f329f5f58..31c92cd8bff 100644 --- a/tests/classes/publication/PublicationTest.php +++ b/tests/classes/publication/PublicationTest.php @@ -22,9 +22,9 @@ use PKP\citation\CitationDAO; use PKP\services\PKPSchemaService; use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineDAO; -use PKP\submission\SubmissionKeywordDAO; -use PKP\submission\SubmissionSubjectDAO; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionSubjectVocab; use PKP\tests\PKPTestCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -40,9 +40,6 @@ protected function setUp(): void { parent::setUp(); $this->publication = (new DAO( - new SubmissionKeywordDAO(), - new SubmissionSubjectDAO(), - new SubmissionDisciplineDAO(), new CitationDAO(), new PKPSchemaService() ))->newDataObject(); From 29008092611a05aec68736d14cb00fdbd7088d6c Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 19 Aug 2024 23:43:25 +0600 Subject: [PATCH 03/30] pkp/pkp-lib#10292 typo fixed --- classes/submission/SubmissionAgencyVocab.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/submission/SubmissionAgencyVocab.php b/classes/submission/SubmissionAgencyVocab.php index 4ba5d1c6a7e..f027b7da65a 100644 --- a/classes/submission/SubmissionAgencyVocab.php +++ b/classes/submission/SubmissionAgencyVocab.php @@ -56,7 +56,7 @@ public function scopeGetAgencies( /** * Get an array of all of the submission's agencies */ - public function scoprGetAllUniqueAgencies(Builder $query):array + public function scopeGetAllUniqueAgencies(Builder $query):array { return Repo::controlledVocab()->getAllUniqueBySymbolic( static::CONTROLLED_VOCAB_SUBMISSION_AGENCY @@ -71,7 +71,7 @@ public function scoprGetAllUniqueAgencies(Builder $query):array * @param bool $deleteFirst True iff existing agencies should be removed first. * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 */ - public function scoprInsertAgencies( + public function scopeInsertAgencies( Builder $query, array $agencies, int $publicationId, From 780f3a2ede6a127ec297e60ab0d3ef5aa97956ef Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 20 Aug 2024 09:24:48 +0600 Subject: [PATCH 04/30] pkp/pkp-lib#10292 proper model import for native xml --- .../filter/NativeXmlPKPPublicationFilter.php | 14 +++++++++----- .../filter/PKPPublicationNativeXmlFilter.php | 12 ++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index ec753b9a9af..eb7d47f506f 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -24,6 +24,10 @@ use PKP\filter\Filter; use PKP\filter\FilterGroup; use PKP\plugins\PluginRegistry; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionAgencyVocab; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionSubjectVocab; class NativeXmlPKPPublicationFilter extends NativeImportFilter { @@ -138,7 +142,7 @@ public function handleChildElement($n, $publication) if (in_array($n->tagName, $setterMappings)) { $publication->setData($n->tagName, $value, $locale); } elseif (isset($controlledVocabulariesMappings[$n->tagName])) { - $controlledVocabulariesModel = $submissionKeywordModel = $controlledVocabulariesMappings[$n->tagName][0]; + $controlledVocabulariesModel = $controlledVocabulariesMappings[$n->tagName][0]; $insertFunction = $controlledVocabulariesMappings[$n->tagName][1]; $controlledVocabulary = []; @@ -315,10 +319,10 @@ public function _getLocalizedPublicationFields() public function _getControlledVocabulariesMappings() { return [ - 'keywords' => ['SubmissionKeywordVocab', 'insertKeywords'], - 'agencies' => ['SubmissionAgencyVocab', 'insertAgencies'], - 'disciplines' => ['SubmissionDisciplineVocab', 'insertDisciplines'], - 'subjects' => ['SubmissionSubjectVocab', 'insertSubjects'], + 'keywords' => [SubmissionKeywordVocab::class, 'insertKeywords'], + 'agencies' => [SubmissionAgencyVocab::class, 'insertAgencies'], + 'disciplines' => [SubmissionDisciplineVocab::class, 'insertDisciplines'], + 'subjects' => [SubmissionSubjectVocab::class, 'insertSubjects'], ]; } diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index 9e5a37106cb..c8937b63140 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -28,6 +28,10 @@ use PKP\submission\PKPSubmission; use PKP\submission\Representation; use PKP\submission\RepresentationDAOInterface; +use PKP\submission\SubmissionKeywordVocab; +use PKP\submission\SubmissionAgencyVocab; +use PKP\submission\SubmissionDisciplineVocab; +use PKP\submission\SubmissionSubjectVocab; class PKPPublicationNativeXmlFilter extends NativeExportFilter { @@ -304,10 +308,10 @@ public function addRepresentations($doc, $entityNode, $entity) public function _getControlledVocabulariesMappings() { return [ - 'keywords' => ['SubmissionKeywordVocab', 'getKeywords', 'keyword'], - 'agencies' => ['SubmissionAgencyVocab', 'getAgencies', 'agency'], - 'disciplines' => ['SubmissionDisciplineVocab', 'getDisciplines', 'discipline'], - 'subjects' => ['SubmissionSubjectVocab', 'getSubjects', 'subject'], + 'keywords' => [SubmissionKeywordVocab::class, 'getKeywords', 'keyword'], + 'agencies' => [SubmissionAgencyVocab::class, 'getAgencies', 'agency'], + 'disciplines' => [SubmissionDisciplineVocab::class, 'getDisciplines', 'discipline'], + 'subjects' => [SubmissionSubjectVocab::class, 'getSubjects', 'subject'], ]; } From 8db798879fe5927742bc2ebdae762d9d86fd9366 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 20 Aug 2024 15:56:48 +0600 Subject: [PATCH 05/30] pkp/pkp-lib#10292 fixing unit tests --- classes/controlledVocab/ControlledVocab.php | 5 +- .../controlledVocab/ControlledVocabDAO.php | 274 ------------------ classes/controlledVocab/Repository.php | 21 ++ classes/user/InterestDAO.php | 162 ----------- classes/user/UserInterest.php | 2 +- .../validation/ValidatorControlledVocab.php | 12 +- .../FormValidatorControlledVocabTest.php | 75 ++--- .../classes/metadata/MetadataPropertyTest.php | 22 +- .../ValidatorControlledVocabTest.php | 65 ++--- 9 files changed, 91 insertions(+), 547 deletions(-) delete mode 100644 classes/controlledVocab/ControlledVocabDAO.php delete mode 100644 classes/user/InterestDAO.php diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index 485541a2e8a..ccdc18105bd 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -1,7 +1,4 @@ retrieve('SELECT * FROM controlled_vocabs WHERE controlled_vocab_id = ?', [(int) $controlledVocabId]); - $row = $result->current(); - return $row ? $this->_fromRow((array) $row) : null; - } - - /** - * Fetch a controlled vocab by symbolic info, building it if needed. - * - * @param string $symbolic - * @param int $assocType - * @param int $assocId - * - * @return ControlledVocab - */ - public function _build($symbolic, $assocType = 0, $assocId = 0) - { - // Attempt to fetch an existing controlled vocabulary. - $controlledVocab = $this->getBySymbolic($symbolic, $assocType, $assocId); - if ($controlledVocab) { - return $controlledVocab; - } - - // Attempt to build a new controlled vocabulary. - $controlledVocab = $this->newDataObject(); - $controlledVocab->setSymbolic($symbolic); - $controlledVocab->setAssocType($assocType); - $controlledVocab->setAssocId($assocId); - $id = $this->insertObject($controlledVocab, false); - if ($id !== null) { - return $controlledVocab; - } - - // Presume that an error was a duplicate insert. - // In this case, try to fetch an existing controlled - // vocabulary. - return $this->getBySymbolic($symbolic, $assocType, $assocId); - } - - /** - * Construct a new data object corresponding to this DAO. - * - * @return ControlledVocab - */ - public function newDataObject() - { - return new ControlledVocab(); - } - - /** - * Internal function to return an ControlledVocab object from a row. - * - * @param array $row - * - * @return ControlledVocab - */ - public function _fromRow($row) - { - $controlledVocab = $this->newDataObject(); - $controlledVocab->setId($row['controlled_vocab_id']); - $controlledVocab->setAssocType($row['assoc_type']); - $controlledVocab->setAssocId($row['assoc_id']); - $controlledVocab->setSymbolic($row['symbolic']); - - return $controlledVocab; - } - - /** - * Insert a new ControlledVocab. - * - * @param ControlledVocab $controlledVocab - * - * @return ?int New insert ID on insert, or null on error - */ - public function insertObject($controlledVocab, $dieOnError = true) - { - $success = $this->update( - sprintf('INSERT INTO controlled_vocabs - (symbolic, assoc_type, assoc_id) - VALUES - (?, ?, ?)'), - [ - $controlledVocab->getSymbolic(), - (int) $controlledVocab->getAssocType(), - (int) $controlledVocab->getAssocId() - ], - true, // callHooks - $dieOnError - ); - if ($success) { - $controlledVocab->setId($this->getInsertId()); - return $controlledVocab->getId(); - } else { - return null; - } // An error occurred on insert - } - - /** - * Update an existing controlled vocab. - * - * @param ControlledVocab $controlledVocab - * - * @return bool - */ - public function updateObject($controlledVocab) - { - $returner = $this->update( - sprintf('UPDATE controlled_vocabs - SET symbolic = ?, - assoc_type = ?, - assoc_id = ? - WHERE controlled_vocab_id = ?'), - [ - $controlledVocab->getSymbolic(), - (int) $controlledVocab->getAssocType(), - (int) $controlledVocab->getAssocId(), - (int) $controlledVocab->getId() - ] - ); - return $returner; - } - - /** - * Delete a controlled vocab. - * - * @param ControlledVocab $controlledVocab - * - * @return bool - */ - public function deleteObject($controlledVocab) - { - return $this->deleteObjectById($controlledVocab->getId()); - } - - /** - * Delete a controlled vocab by controlled vocab ID. - * - * @param int $controlledVocabId - * - * @return bool - */ - public function deleteObjectById($controlledVocabId) - { - $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); /** @var ControlledVocabEntryDAO $controlledVocabEntryDao */ - $controlledVocabEntries = $this->enumerate($controlledVocabId); - foreach ($controlledVocabEntries as $controlledVocabEntryId => $controlledVocabEntryName) { - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId); - } - return $this->update('DELETE FROM controlled_vocabs WHERE controlled_vocab_id = ?', [(int) $controlledVocabId]); - } - - /** - * Retrieve an array of controlled vocabs matching the specified - * symbolic name and assoc info. - * - * @param string $symbolic - * @param int $assocType - * @param int $assocId - * - * @return ?ControlledVocab - */ - public function getBySymbolic($symbolic, $assocType = 0, $assocId = 0) - { - $result = $this->retrieve( - 'SELECT * FROM controlled_vocabs WHERE symbolic = ? AND assoc_type = ? AND assoc_id = ?', - [$symbolic, (int) $assocType, (int) $assocId] - ); - $row = $result->current(); - return $row ? $this->_fromRow((array) $row) : null; - } - - /** - * Get a list of controlled vocabulary options. - * - * @param string $symbolic - * @param int $assocType - * @param int $assocId - * @param string $settingName optional - * - * @return array $controlledVocabEntryId => $settingValue - */ - public function enumerateBySymbolic($symbolic, $assocType, $assocId, $settingName = 'name') - { - $controlledVocab = $this->getBySymbolic($symbolic, $assocType, $assocId); - if (!$controlledVocab) { - return []; - } - return $controlledVocab->enumerate($settingName); - } - - /** - * Get a list of controlled vocabulary options. - * - * @param int $controlledVocabId - * @param string $settingName optional - * - * @return array $controlledVocabEntryId => name - */ - public function enumerate($controlledVocabId, $settingName = 'name') - { - $result = $this->retrieve( - 'SELECT e.controlled_vocab_entry_id, - COALESCE(l.setting_value, p.setting_value, n.setting_value) AS setting_value, - COALESCE(l.setting_type, p.setting_type, n.setting_type) AS setting_type - FROM controlled_vocab_entries e - LEFT JOIN controlled_vocab_entry_settings l ON (l.controlled_vocab_entry_id = e.controlled_vocab_entry_id AND l.setting_name = ? AND l.locale = ?) - LEFT JOIN controlled_vocab_entry_settings p ON (p.controlled_vocab_entry_id = e.controlled_vocab_entry_id AND p.setting_name = ? AND p.locale = ?) - LEFT JOIN controlled_vocab_entry_settings n ON (n.controlled_vocab_entry_id = e.controlled_vocab_entry_id AND n.setting_name = ? AND n.locale = ?) - WHERE e.controlled_vocab_id = ? - ORDER BY e.seq', - [ - $settingName, Locale::getLocale(), // Current locale - $settingName, Locale::getPrimaryLocale(), // Primary locale - $settingName, '', // No locale - (int) $controlledVocabId - ] - ); - - $returner = []; - foreach ($result as $row) { - $returner[$row->controlled_vocab_entry_id] = $this->convertFromDB( - $row->setting_value, - $row->setting_type - ); - } - return $returner; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\controlledVocab\ControlledVocabDAO', '\ControlledVocabDAO'); -} diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 43f15ad513a..06dfad4c0ea 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -1,5 +1,17 @@ build(); - $result = $this->retrieveRange( - 'SELECT cve.controlled_vocab_entry_id FROM controlled_vocab_entries cve, user_interests ui WHERE cve.controlled_vocab_id = ? AND ui.controlled_vocab_entry_id = cve.controlled_vocab_entry_id AND ui.user_id = ?', - [(int) $controlledVocab->getId(), (int) $userId] - ); - - $ids = []; - foreach ($result as $row) { - $ids[] = $row->controlled_vocab_entry_id; - } - return $ids; - } - - /** - * Get a list of user IDs attributed to an interest - * - * @return array - */ - public function getUserIdsByInterest($interest) - { - $result = $this->retrieve( - ' - SELECT ui.user_id - FROM user_interests ui - INNER JOIN controlled_vocab_entry_settings cves ON (ui.controlled_vocab_entry_id = cves.controlled_vocab_entry_id) - WHERE cves.setting_name = ? AND cves.setting_value = ?', - ['interest', $interest] - ); - - $returner = []; - foreach ($result as $row) { - $returner[] = $row->user_id; - } - return $returner; - } - - /** - * Get all user's interests - * - * @param string $filter (optional) - * - * @return object - */ - public function getAllInterests($filter = null) - { - $controlledVocab = $this->build(); - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ - $iterator = $interestEntryDao->getByControlledVocabId($controlledVocab->getId(), null, $filter); - - // Sort by name. - $interests = $iterator->toArray(); - usort($interests, function ($s1, $s2) { - return strcmp($s1->getInterest(), $s2->getInterest()); - }); - - // Turn back into an iterator. - return new ArrayItemIterator($interests); - } - - /** - * Update a user's set of interests - * - * @param array $interests - * @param int $userId - */ - public function setUserInterests($interests, $userId) - { - // Remove duplicates - $interests ??= []; - $interests = array_unique($interests); - - // Trim whitespace - $interests = array_map(trim(...), $interests); - - // Delete the existing interests association. - $this->update( - 'DELETE FROM user_interests WHERE user_id = ?', - [(int) $userId] - ); - - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ - $controlledVocab = $this->build(); - - // Store the new interests. - foreach ((array) $interests as $interest) { - $interestEntry = $interestEntryDao->getBySetting( - $interest, - $controlledVocab->getSymbolic(), - $controlledVocab->getAssocId(), - $controlledVocab->getAssocType(), - $controlledVocab->getSymbolic() - ); - - if (!$interestEntry) { - $interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */ - $interestEntry->setInterest($interest); - $interestEntry->setControlledVocabId($controlledVocab->getId()); - $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); - } - - $entry = [ - [ 'user_id' => (int) $userId, 'controlled_vocab_entry_id' => (int) $interestEntry->getId()] - ]; - DB::table('user_interests')->insertOrIgnore( - $entry - ); - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\user\InterestDAO', '\InterestDAO'); - define('CONTROLLED_VOCAB_INTEREST', InterestDAO::CONTROLLED_VOCAB_INTEREST); -} diff --git a/classes/user/UserInterest.php b/classes/user/UserInterest.php index 67d23d01b41..720c58c6855 100644 --- a/classes/user/UserInterest.php +++ b/classes/user/UserInterest.php @@ -9,7 +9,7 @@ * * @class UserInterest * - * @brief + * @brief UserInterest model calss */ namespace PKP\user; diff --git a/classes/validation/ValidatorControlledVocab.php b/classes/validation/ValidatorControlledVocab.php index 93600854a53..ebd9539c303 100644 --- a/classes/validation/ValidatorControlledVocab.php +++ b/classes/validation/ValidatorControlledVocab.php @@ -3,8 +3,8 @@ /** * @file classes/validation/ValidatorControlledVocab.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2000-2024 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class ValidatorControlledVocab @@ -31,8 +31,8 @@ public function __construct(string $symbolic, int $assocType, int $assocId) $controlledVocab = ControlledVocab::withSymbolic($symbolic) ->withAssoc($assocType, $assocId) ->first(); - - $this->acceptedValues = $controlledVocab?->enumerate() ?? []; + + $this->acceptedValues = array_keys($controlledVocab?->enumerate() ?? []); } // @@ -48,7 +48,3 @@ public function isValid($value): bool return in_array($value, $this->acceptedValues); } } - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\validation\ValidatorControlledVocab', '\ValidatorControlledVocab'); -} diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index 6d88820df87..8c7ab043641 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -3,75 +3,53 @@ /** * @file tests/classes/form/validation/FormValidatorControlledVocabTest.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2000-2024 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class FormValidatorControlledVocabTest * - * @ingroup tests_classes_form_validation - * - * @see FormValidatorControlledVocab - * * @brief Test class for FormValidatorControlledVocab. */ namespace PKP\tests\classes\form\validation; use APP\core\Application; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\MockObject\MockObject; -use PKP\form\validation\FormValidatorControlledVocab; -use PKP\controlledVocab\ControlledVocab; -use PKP\controlledVocab\ControlledVocabDAO; use PKP\db\DAORegistry; +use PKP\controlledVocab\ControlledVocabEntryDAO; +use APP\facades\Repo; +use PKP\form\validation\FormValidatorControlledVocab; use PKP\form\Form; use PKP\form\validation\FormValidator; use PKP\tests\PKPTestCase; +use PHPUnit\Framework\Attributes\CoversClass; #[CoversClass(FormValidatorControlledVocab::class)] class FormValidatorControlledVocabTest extends PKPTestCase { - /** - * @see PKPTestCase::getMockedDAOs() - */ - protected function getMockedDAOs(): array - { - return [...parent::getMockedDAOs()]; - } - public function testIsValid() { // Test form $form = new Form('some template'); - // Mock a ControlledVocab object - /** @var ControlledVocab|MockObject */ - $mockControlledVocab = $this->getMockBuilder(ControlledVocab::class) - ->onlyMethods(['enumerate']) - ->getMock(); - $mockControlledVocab->setId(1); - $mockControlledVocab->setAssocType(Application::ASSOC_TYPE_CITATION); - $mockControlledVocab->setAssocId(333); - $mockControlledVocab->setSymbolic('testVocab'); - - // Set up the mock enumerate() method - $mockControlledVocab->expects($this->any()) - ->method('enumerate') - ->willReturn([1 => 'vocab1', 2 => 'vocab2']); + $testControlledVocab = Repo::controlledVocab()->build( + 'testVocab', + Application::ASSOC_TYPE_CITATION, + 333 + ); - // Mock the ControlledVocabDAO - $mockControlledVocabDao = $this->getMockBuilder(ControlledVocabDAO::class) - ->onlyMethods(['getBySymbolic']) - ->getMock(); + /** @var ControlledVocabEntryDAO */ + $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); - // Set up the mock getBySymbolic() method - $mockControlledVocabDao->expects($this->any()) - ->method('getBySymbolic') - ->with('testVocab', Application::ASSOC_TYPE_CITATION, 333) - ->willReturn($mockControlledVocab); + $testControlledVocabEntry1 = $controlledVocabEntryDao->newDataObject(); + $testControlledVocabEntry1->setName('testEntry', 'en'); + $testControlledVocabEntry1->setControlledVocabId($testControlledVocab->id); + $controlledVocabEntryId1 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry1); - DAORegistry::registerDAO('ControlledVocabDAO', $mockControlledVocabDao); + $testControlledVocabEntry2 = $controlledVocabEntryDao->newDataObject(); + $testControlledVocabEntry2->setName('testEntry', 'en'); + $testControlledVocabEntry2->setControlledVocabId($testControlledVocab->id); + $controlledVocabEntryId2 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry2); // Instantiate validator $validator = new FormValidatorControlledVocab( @@ -84,13 +62,18 @@ public function testIsValid() 333 ); - $form->setData('testData', '1'); + $form->setData('testData', $controlledVocabEntryId1); self::assertTrue($validator->isValid()); - $form->setData('testData', '2'); + $form->setData('testData', $controlledVocabEntryId2); self::assertTrue($validator->isValid()); - $form->setData('testData', '3'); + $form->setData('testData', 3); self::assertFalse($validator->isValid()); + + // Delete the test entried + $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId1); + $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId2); + $testControlledVocab->delete(); } } diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index b1c2004909b..a8c79d7e17d 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -18,8 +18,8 @@ namespace PKP\tests\classes\metadata; +use APP\facades\Repo; use InvalidArgumentException; -use PKP\controlledVocab\ControlledVocabDAO; use PKP\controlledVocab\ControlledVocabEntryDAO; use PKP\db\DAORegistry; use PKP\metadata\MetadataDescription; @@ -148,26 +148,32 @@ public function testValidateControlledVocabulary() { // Build a test vocabulary. (Assoc type and id are 0 to // simulate a site-wide vocabulary). - /** @var ControlledVocabDAO */ - $controlledVocabDao = DAORegistry::getDAO('ControlledVocabDAO'); - $testControlledVocab = $controlledVocabDao->_build('test-controlled-vocab', 0, 0); + $testControlledVocab = Repo::controlledVocab()->build('test-controlled-vocab', 0, 0); // Make a vocabulary entry /** @var ControlledVocabEntryDAO */ $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); $testControlledVocabEntry = $controlledVocabEntryDao->newDataObject(); $testControlledVocabEntry->setName('testEntry', 'en'); - $testControlledVocabEntry->setControlledVocabId($testControlledVocab->getId()); + $testControlledVocabEntry->setControlledVocabId($testControlledVocab->id); $controlledVocabEntryId = $controlledVocabEntryDao->insertObject($testControlledVocabEntry); - $metadataProperty = new MetadataProperty('testElement', [], [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab']); + $metadataProperty = new MetadataProperty( + 'testElement', + [], + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab'] + ); // This validator checks numeric values - self::assertEquals([MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab'], $metadataProperty->isValid($controlledVocabEntryId)); + self::assertEquals( + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab'], + $metadataProperty->isValid($controlledVocabEntryId) + ); self::assertFalse($metadataProperty->isValid($controlledVocabEntryId + 1)); // Delete the test vocabulary - $controlledVocabDao->deleteObject($testControlledVocab); + $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId); + $testControlledVocab->delete(); } public function testValidateDate() diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index a7bed4d5b56..7cef5d0bf30 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -3,26 +3,20 @@ /** * @file tests/classes/validation/ValidatorControlledVocabTest.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2000-2024 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class ValidatorControlledVocabTest * - * @ingroup tests_classes_validation - * - * @see ValidatorControlledVocab - * * @brief Test class for ValidatorControlledVocab. */ namespace PKP\tests\classes\validation; use APP\core\Application; +use APP\facades\Repo; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\MockObject\MockObject; -use PKP\controlledVocab\ControlledVocab; -use PKP\controlledVocab\ControlledVocabDAO; use PKP\db\DAORegistry; use PKP\tests\PKPTestCase; use PKP\validation\ValidatorControlledVocab; @@ -30,47 +24,30 @@ #[CoversClass(ValidatorControlledVocab::class)] class ValidatorControlledVocabTest extends PKPTestCase { - /** - * @see PKPTestCase::getMockedDAOs() - */ - protected function getMockedDAOs(): array - { - return [...parent::getMockedDAOs(), 'ControlledVocabDAO']; - } - public function testValidatorControlledVocab() { - // Mock a ControlledVocab object - /** @var ControlledVocab|MockObject */ - $mockControlledVocab = $this->getMockBuilder(ControlledVocab::class) - ->onlyMethods(['enumerate']) - ->getMock(); - $mockControlledVocab->setId(1); - $mockControlledVocab->setAssocType(Application::ASSOC_TYPE_CITATION); - $mockControlledVocab->setAssocId(333); - $mockControlledVocab->setSymbolic('testVocab'); - - // Set up the mock enumerate() method - $mockControlledVocab->expects($this->any()) - ->method('enumerate') - ->willReturn([1 => 'vocab1', 2 => 'vocab2']); + $testControlledVocab = Repo::controlledVocab()->build( + 'testVocab', + Application::ASSOC_TYPE_CITATION, + 333 + ); - // Mock the ControlledVocabDAO - $mockControlledVocabDao = $this->getMockBuilder(ControlledVocabDAO::class) - ->onlyMethods(['getBySymbolic']) - ->getMock(); + /** @var ControlledVocabEntryDAO */ + $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); - // Set up the mock getBySymbolic() method - $mockControlledVocabDao->expects($this->any()) - ->method('getBySymbolic') - ->with('testVocab', Application::ASSOC_TYPE_CITATION, 333) - ->willReturn($mockControlledVocab); + $testControlledVocabEntry1 = $controlledVocabEntryDao->newDataObject(); + $testControlledVocabEntry1->setName('testEntry', 'en'); + $testControlledVocabEntry1->setControlledVocabId($testControlledVocab->id); + $controlledVocabEntryId1 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry1); - DAORegistry::registerDAO('ControlledVocabDAO', $mockControlledVocabDao); + $testControlledVocabEntry2 = $controlledVocabEntryDao->newDataObject(); + $testControlledVocabEntry2->setName('testEntry', 'en'); + $testControlledVocabEntry2->setControlledVocabId($testControlledVocab->id); + $controlledVocabEntryId2 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry2); $validator = new ValidatorControlledVocab('testVocab', Application::ASSOC_TYPE_CITATION, 333); - self::assertTrue($validator->isValid('1')); - self::assertTrue($validator->isValid('2')); - self::assertFalse($validator->isValid('3')); + self::assertTrue($validator->isValid($controlledVocabEntryId1)); + self::assertTrue($validator->isValid($controlledVocabEntryId2)); + self::assertFalse($validator->isValid(3)); } } From e54f0b0e75861bdcd62974d0626d7acd46c9a612 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 26 Aug 2024 15:42:49 +0600 Subject: [PATCH 06/30] pkp/pkp-lib#10292 Refactored user interest to model and repository --- classes/controlledVocab/Repository.php | 49 ------ classes/facades/Repo.php | 6 + classes/user/InterestManager.php | 8 +- .../Repository.php} | 144 ++++++++---------- classes/user/interest/UserInterest.php | 79 ++++++++++ classes/user/maps/Schema.php | 3 +- 6 files changed, 151 insertions(+), 138 deletions(-) rename classes/user/{UserInterest.php => interest/Repository.php} (50%) create mode 100644 classes/user/interest/UserInterest.php diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 06dfad4c0ea..31a546853c6 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -14,12 +14,8 @@ namespace PKP\controlledVocab; -use APP\facades\Repo; use Illuminate\Support\Facades\DB; use PKP\db\DAORegistry; -use PKP\user\UserInterest; -use PKP\user\InterestEntry; -use PKP\user\InterestEntryDAO; use PKP\controlledVocab\ControlledVocab; use PKP\controlledVocab\ControlledVocabEntryDAO; @@ -151,49 +147,4 @@ public function insertBySymbolic( } } } - - /** - * Update a user's set of interests - */ - public function setUserInterests(array $interests, int $userId): void - { - $controlledVocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST - ); - - /** @var InterestEntryDAO $interestEntryDao */ - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); - - DB::beginTransaction(); - - // Delete the existing interests association. - UserInterest::withUserId($userId)->delete(); - - collect($interests) - ->map(fn (string $interest): string => trim($interest)) - ->unique() - ->each(function (string $interest) use ($controlledVocab, $interestEntryDao, $userId): void { - $interestEntry = $interestEntryDao->getBySetting( - $interest, - $controlledVocab->symbolic, - $controlledVocab->assocId, - $controlledVocab->assocType, - $controlledVocab->symbolic - ); - - if (!$interestEntry) { - $interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */ - $interestEntry->setInterest($interest); - $interestEntry->setControlledVocabId($controlledVocab->id); - $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); - } - - UserInterest::create([ - 'userId' => $userId, - 'controlledVocabEntryId' => $interestEntry->getId(), - ]); - }); - - DB::commit(); - } } diff --git a/classes/facades/Repo.php b/classes/facades/Repo.php index 8a4df6f8f4a..8623b3b921f 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -44,6 +44,7 @@ use PKP\stageAssignment\Repository as StageAssignmentRepository; use PKP\submissionFile\Repository as SubmissionFileRepository; use PKP\userGroup\Repository as UserGroupRepository; +use PKP\user\interest\Repository as UserInterestRepository; class Repo { @@ -146,4 +147,9 @@ public static function controlledVocab(): ControlledVocabRepository { return app(ControlledVocabRepository::class); } + + public static function userInterest(): UserInterestRepository + { + return app(UserInterestRepository::class); + } } diff --git a/classes/user/InterestManager.php b/classes/user/InterestManager.php index 0090c978201..5a918441b1c 100644 --- a/classes/user/InterestManager.php +++ b/classes/user/InterestManager.php @@ -17,7 +17,7 @@ use APP\facades\Repo; use PKP\db\DAORegistry; use PKP\user\User; -use PKP\user\UserInterest; +use PKP\user\interest\UserInterest; class InterestManager { @@ -26,7 +26,7 @@ class InterestManager */ public function getAllInterests(?string $filter = null): array { - $interests = UserInterest::getAllInterests($filter); + $interests = Repo::userInterest()->getAllInterests($filter); $interestReturner = []; while ($interest = $interests->next()) { @@ -46,7 +46,7 @@ public function getInterestsForUser(User $user): array $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ $controlledVocab = Repo::controlledVocab()->build(UserInterest::CONTROLLED_VOCAB_INTEREST); - foreach (UserInterest::getUserInterestIds($user->getId()) as $interestEntryId) { + foreach (Repo::userInterest()->getUserInterestIds($user->getId()) as $interestEntryId) { /** @var InterestEntry */ $interestEntry = $interestsCache[$interestEntryId] ??= $interestEntryDao->getById( $interestEntryId, @@ -79,7 +79,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ? $interests : (empty($interests) ? [] : explode(',', $interests)); - Repo::controlledVocab()->setUserInterests($interests, $user->getId()); + Repo::userInterest()->setUserInterests($interests, $user->getId()); } } diff --git a/classes/user/UserInterest.php b/classes/user/interest/Repository.php similarity index 50% rename from classes/user/UserInterest.php rename to classes/user/interest/Repository.php index 720c58c6855..8fb8c304696 100644 --- a/classes/user/UserInterest.php +++ b/classes/user/interest/Repository.php @@ -1,106 +1,39 @@ 'integer', - 'controlled_vocab_entry_id' => 'integer', - ]; - } - - /** - * Accessor and Mutator for primary key => id - */ - protected function id(): Attribute - { - return Attribute::make( - get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, - set: fn($value) => [$this->primaryKey => $value], - ); - } - - /** - * Compatibility function for including note IDs in grids. - * - * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. - */ - public function getId(): int - { - return $this->id; - } - - /** - * Compatibility function for including notes in grids. - * - * @deprecated 3.5. Use $model or $model->$field instead. Can be removed once the DataObject pattern is removed. - */ - public function getData(?string $field): mixed - { - return $field ? $this->$field : $this; - } - - /** - * Scope a query to only include notes with a specific assoc type and assoc ID. - */ - public function scopeWithUserId(Builder $query, int $userId): Builder - { - return $query->where('user_id', $userId); - } /** * Get a list of controlled vocabulary entry IDs (corresponding to interest keywords) * attributed to a user */ - public static function getUserInterestIds(int $userId): array + public function getUserInterestIds(int $userId): array { $controlledVocab = Repo::controlledVocab()->build( - static::CONTROLLED_VOCAB_INTEREST + UserInterest::CONTROLLED_VOCAB_INTEREST ); return DB::table('controlled_vocab_entries AS cve') @@ -120,7 +53,7 @@ public static function getUserInterestIds(int $userId): array /** * Get a list of user IDs attributed to an interest */ - public static function getUserIdsByInterest(string $interest): array + public function getUserIdsByInterest(string $interest): array { return DB::table('user_interests AS ui') ->select('ui.user_id') @@ -128,7 +61,7 @@ public static function getUserIdsByInterest(string $interest): array 'controlled_vocab_entry_settings AS cves', fn (JoinClause $join) => $join ->on('cves.controlled_vocab_entry_id', '=', 'ui.controlled_vocab_entry_id') - ->where('cves.setting_name', STATIC::CONTROLLED_VOCAB_INTEREST) + ->where('cves.setting_name', UserInterest::CONTROLLED_VOCAB_INTEREST) ->where(DB::raw('LOWER(cves.setting_value)'), trim(strtolower($interest))) ) ->get() @@ -140,10 +73,10 @@ public static function getUserIdsByInterest(string $interest): array /** * Get all user's interests */ - public static function getAllInterests(?string $filter = null): object + public function getAllInterests(?string $filter = null): object { $controlledVocab = Repo::controlledVocab()->build( - static::CONTROLLED_VOCAB_INTEREST + UserInterest::CONTROLLED_VOCAB_INTEREST ); $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ @@ -158,4 +91,49 @@ public static function getAllInterests(?string $filter = null): object // Turn back into an iterator. return new ArrayItemIterator($interests); } + + /** + * Update a user's set of interests + */ + public function setUserInterests(array $interests, int $userId): void + { + $controlledVocab = Repo::controlledVocab()->build( + UserInterest::CONTROLLED_VOCAB_INTEREST + ); + + /** @var InterestEntryDAO $interestEntryDao */ + $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); + + DB::beginTransaction(); + + // Delete the existing interests association. + UserInterest::withUserId($userId)->delete(); + + collect($interests) + ->map(fn (string $interest): string => trim($interest)) + ->unique() + ->each(function (string $interest) use ($controlledVocab, $interestEntryDao, $userId): void { + $interestEntry = $interestEntryDao->getBySetting( + $interest, + $controlledVocab->symbolic, + $controlledVocab->assocId, + $controlledVocab->assocType, + $controlledVocab->symbolic + ); + + if (!$interestEntry) { + $interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */ + $interestEntry->setInterest($interest); + $interestEntry->setControlledVocabId($controlledVocab->id); + $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); + } + + UserInterest::create([ + 'userId' => $userId, + 'controlledVocabEntryId' => $interestEntry->getId(), + ]); + }); + + DB::commit(); + } } diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php new file mode 100644 index 00000000000..e8c40e4fe04 --- /dev/null +++ b/classes/user/interest/UserInterest.php @@ -0,0 +1,79 @@ + 'integer', + 'controlled_vocab_entry_id' => 'integer', + ]; + } + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn($value) => [$this->primaryKey => $value], + ); + } + + /** + * Compatibility function for including note IDs in grids. + * + * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. + */ + public function getId(): int + { + return $this->id; + } + + /** + * Scope a query to only include notes with a specific assoc type and assoc ID. + */ + public function scopeWithUserId(Builder $query, int $userId): Builder + { + return $query->where('user_id', $userId); + } +} diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index e41169fca1c..b43bd08050f 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -16,7 +16,6 @@ use APP\facades\Repo; use APP\submission\Submission; use Illuminate\Support\Enumerable; -use PKP\user\UserInterest; use PKP\db\DAORegistry; use PKP\plugins\Hook; use PKP\security\Role; @@ -187,7 +186,7 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat case 'interests': $output[$prop] = []; if ($this->context) { - $interestEntryIds = UserInterest::getUserInterestIds($user->getId()); + $interestEntryIds = Repo::userInterest()->getUserInterestIds($user->getId()); if (!empty($interestEntryIds)) { $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var \PKP\user\InterestEntryDAO $interestEntryDao */ $results = $interestEntryDao->getByIds($interestEntryIds); From f54ff871126b788336f35ca73a8a5205131dfa3d Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 26 Aug 2024 19:04:01 +0600 Subject: [PATCH 07/30] pkp/pkp-lib#10292 removed extended vocab classes --- api/v1/vocabs/PKPVocabController.php | 33 ++----- .../components/forms/publication/Details.php | 4 +- .../forms/publication/PKPMetadataForm.php | 13 +-- classes/controlledVocab/ControlledVocab.php | 45 ++++++--- classes/publication/DAO.php | 84 +++++++++++++---- classes/submission/SubmissionAgencyVocab.php | 90 ------------------ .../submission/SubmissionDisciplineVocab.php | 92 ------------------ classes/submission/SubmissionKeywordVocab.php | 94 ------------------- classes/submission/SubmissionSubjectVocab.php | 90 ------------------ .../filter/NativeXmlPKPPublicationFilter.php | 26 ++--- .../filter/PKPPublicationNativeXmlFilter.php | 26 ++--- tests/classes/publication/PublicationTest.php | 4 - .../ValidatorControlledVocabTest.php | 5 + 13 files changed, 150 insertions(+), 456 deletions(-) delete mode 100644 classes/submission/SubmissionAgencyVocab.php delete mode 100644 classes/submission/SubmissionDisciplineVocab.php delete mode 100644 classes/submission/SubmissionKeywordVocab.php delete mode 100644 classes/submission/SubmissionSubjectVocab.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 55dda0f32f3..3cb342baec7 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -23,18 +23,14 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Route; +use PKP\controlledVocab\ControlledVocab; use PKP\core\PKPBaseController; use PKP\core\PKPRequest; -use PKP\db\DAORegistry; use PKP\facades\Locale; use PKP\plugins\Hook; use PKP\security\authorization\ContextAccessPolicy; use PKP\security\authorization\UserRolesRequiredPolicy; use PKP\security\Role; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionSubjectVocab; class PKPVocabController extends PKPBaseController { @@ -113,26 +109,13 @@ public function getMany(Request $illuminateRequest): JsonResponse ], Response::HTTP_BAD_REQUEST); } - switch ($vocab) { - case SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD: - $submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var \PKP\submission\SubmissionKeywordEntryDAO $submissionKeywordEntryDao */ - $entries = $submissionKeywordEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); - break; - case SubmissionSubjectVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT: - $submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var \PKP\submission\SubmissionSubjectEntryDAO $submissionSubjectEntryDao */ - $entries = $submissionSubjectEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); - break; - case SubmissionDisciplineVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE: - $submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var \PKP\submission\SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */ - $entries = $submissionDisciplineEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); - break; - case SubmissionAgencyVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY: - $submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var \PKP\submission\SubmissionAgencyEntryDAO $submissionAgencyEntryDao */ - $entries = $submissionAgencyEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); - break; - default: - $entries = []; - Hook::call('API::vocabs::getMany', [$vocab, &$entries, $illuminateRequest, response(), $request]); + if (ControlledVocab::hasDefinedVocabSymbolic($vocab)) { + /** @var \PKP\controlledVocab\ControlledVocabEntryDAO $entryDao */ + $entryDao = Repo::controlledVocab()->getEntryDaoBySymbolic($vocab); + $entries = $entryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); + } else { + $entries = []; + Hook::call('API::vocabs::getMany', [$vocab, &$entries, $illuminateRequest, response(), $request]); } $data = []; diff --git a/classes/components/forms/publication/Details.php b/classes/components/forms/publication/Details.php index b03b3d7bed8..76229c2c361 100644 --- a/classes/components/forms/publication/Details.php +++ b/classes/components/forms/publication/Details.php @@ -18,7 +18,7 @@ use APP\publication\Publication; use PKP\components\forms\FieldControlledVocab; use PKP\context\Context; -use PKP\submission\SubmissionKeywordVocab; +use PKP\controlledVocab\ControlledVocab; class Details extends TitleAbstractForm { @@ -46,7 +46,7 @@ public function __construct( 'label' => __('common.keywords'), 'description' => __('manager.setup.metadata.keywords.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('keywords'), 'isRequired' => $context->getData('keywords') === Context::METADATA_REQUIRE ? true : false, diff --git a/classes/components/forms/publication/PKPMetadataForm.php b/classes/components/forms/publication/PKPMetadataForm.php index c7dd0f8ade9..f36082548be 100644 --- a/classes/components/forms/publication/PKPMetadataForm.php +++ b/classes/components/forms/publication/PKPMetadataForm.php @@ -16,15 +16,12 @@ namespace PKP\components\forms\publication; use APP\publication\Publication; +use PKP\controlledVocab\ControlledVocab; use PKP\components\forms\FieldControlledVocab; use PKP\components\forms\FieldRichTextarea; use PKP\components\forms\FieldText; use PKP\components\forms\FormComponent; use PKP\context\Context; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionSubjectVocab; class PKPMetadataForm extends FormComponent { @@ -55,7 +52,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('common.keywords'), 'tooltip' => __('manager.setup.metadata.keywords.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionKeywordVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('keywords'), ])); @@ -66,7 +63,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('common.subjects'), 'tooltip' => __('manager.setup.metadata.subjects.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionSubjectVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('subjects'), ])); @@ -77,7 +74,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('search.discipline'), 'tooltip' => __('manager.setup.metadata.disciplines.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionDisciplineVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('disciplines'), ])); @@ -88,7 +85,7 @@ public function __construct(string $action, array $locales, Publication $publica 'label' => __('submission.supportingAgencies'), 'tooltip' => __('manager.setup.metadata.agencies.description'), 'isMultilingual' => true, - 'apiUrl' => str_replace('__vocab__', SubmissionAgencyVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $suggestionUrlBase), + 'apiUrl' => str_replace('__vocab__', ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $suggestionUrlBase), 'locales' => $this->locales, 'value' => (array) $publication->getData('supportingAgencies'), ])); diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index ccdc18105bd..c4bf848ebde 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -26,6 +26,12 @@ class ControlledVocab extends Model { use HasCamelCasing; + public const CONTROLLED_VOCAB_SUBMISSION_AGENCY = 'submissionAgency'; + public const CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE = 'submissionDiscipline'; + public const CONTROLLED_VOCAB_SUBMISSION_KEYWORD = 'submissionKeyword'; + public const CONTROLLED_VOCAB_SUBMISSION_LANGUAGE = 'submissionLanguage'; + public const CONTROLLED_VOCAB_SUBMISSION_SUBJECT = 'submissionSubject'; + protected $table = 'controlled_vocabs'; protected $primaryKey = 'controlled_vocab_id'; @@ -40,6 +46,22 @@ class ControlledVocab extends Model */ public $timestamps = false; + public static function getDefinedVocabSymbolic(): array + { + return [ + static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + static::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE, + static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + ]; + } + + public static function hasDefinedVocabSymbolic(string $vocab): bool + { + return in_array($vocab, static::getDefinedVocabSymbolic()); + } + protected function casts(): array { return [ @@ -60,6 +82,17 @@ protected function id(): Attribute ); } + public function getLocaleFieldNames(): array + { + if (!$this->symbolic) { + return []; + } + + return static::hasDefinedVocabSymbolic($this->symbolic) + ? [$this->symbolic] + : []; + } + /** * Compatibility function for including note IDs in grids. * @@ -70,22 +103,12 @@ public function getId(): int return $this->id; } - /** - * Compatibility function for including notes in grids. - * - * @deprecated 3.5. Use $model or $model->$field instead. Can be removed once the DataObject pattern is removed. - */ - public function getData(?string $field): mixed - { - return $field ? $this->$field : $this; - } - /** * Scope a query to only include notes with a specific user ID. */ public function scopeWithSymbolic(Builder $query, string $symbolic): Builder { - return $query->where(DB::raw('LOWER(symbolic)'), strtolower($symbolic)); + return $query->where('symbolic', $symbolic); } /** diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 4c6cab99e90..31d874720eb 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -14,19 +14,17 @@ namespace PKP\publication; use APP\facades\Repo; +use APP\core\Application; use APP\publication\Publication; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\Facades\DB; use Illuminate\Support\LazyCollection; use PKP\citation\CitationDAO; +use PKP\controlledVocab\ControlledVocab; use PKP\core\EntityDAO; use PKP\core\traits\EntityWithParent; use PKP\services\PKPSchemaService; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionSubjectVocab; /** * @template T of Publication @@ -348,10 +346,41 @@ protected function deleteAuthors(int $publicationId) */ protected function setControlledVocab(Publication $publication) { - $publication->setData('keywords', SubmissionKeywordVocab::getKeywords($publication->getId())); - $publication->setData('subjects', SubmissionSubjectVocab::getSubjects($publication->getId())); - $publication->setData('disciplines', SubmissionDisciplineVocab::getDisciplines($publication->getId())); - $publication->setData('supportingAgencies', SubmissionAgencyVocab::getAgencies($publication->getId())); + $publication->setData( + 'keywords', + Repo::controlledVocab()->getBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + Application::ASSOC_TYPE_PUBLICATION, + $publication->getId() + ) + ); + + $publication->setData( + 'subjects', + Repo::controlledVocab()->getBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + Application::ASSOC_TYPE_PUBLICATION, + $publication->getId() + ) + ); + + $publication->setData( + 'disciplines', + Repo::controlledVocab()->getBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + Application::ASSOC_TYPE_PUBLICATION, + $publication->getId() + ) + ); + + $publication->setData( + 'supportingAgencies', + Repo::controlledVocab()->getBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + Application::ASSOC_TYPE_PUBLICATION, + $publication->getId() + ) + ); } /** @@ -389,10 +418,10 @@ protected function saveControlledVocab(array $values, int $publicationId) // Update controlled vocabularly for which we have props foreach ($values as $prop => $value) { match ($prop) { - 'keywords' => SubmissionKeywordVocab::insertKeywords($value, $publicationId), - 'subjects' => SubmissionSubjectVocab::insertSubjects($value, $publicationId), - 'disciplines' => SubmissionDisciplineVocab::insertDisciplines($value, $publicationId), - 'supportingAgencies' => SubmissionAgencyVocab::insertAgencies($value, $publicationId), + 'keywords' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'subjects' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'disciplines' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'supportingAgencies' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), }; } } @@ -402,10 +431,33 @@ protected function saveControlledVocab(array $values, int $publicationId) */ protected function deleteControlledVocab(int $publicationId) { - SubmissionKeywordVocab::insertKeywords([], $publicationId); - SubmissionSubjectVocab::insertSubjects([], $publicationId); - SubmissionDisciplineVocab::insertDisciplines([], $publicationId); - SubmissionAgencyVocab::insertAgencies([], $publicationId); + Repo::controlledVocab()->insertBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + [], + Application::ASSOC_TYPE_PUBLICATION, + $publicationId + ); + + Repo::controlledVocab()->insertBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + [], + Application::ASSOC_TYPE_PUBLICATION, + $publicationId + ); + + Repo::controlledVocab()->insertBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + [], + Application::ASSOC_TYPE_PUBLICATION, + $publicationId + ); + + Repo::controlledVocab()->insertBySymbolic( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + [], + Application::ASSOC_TYPE_PUBLICATION, + $publicationId + ); } /** diff --git a/classes/submission/SubmissionAgencyVocab.php b/classes/submission/SubmissionAgencyVocab.php deleted file mode 100644 index f027b7da65a..00000000000 --- a/classes/submission/SubmissionAgencyVocab.php +++ /dev/null @@ -1,90 +0,0 @@ -getBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, - $assocType, - $publicationId, - $locales - ); - } - - /** - * Get an array of all of the submission's agencies - */ - public function scopeGetAllUniqueAgencies(Builder $query):array - { - return Repo::controlledVocab()->getAllUniqueBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_AGENCY - ); - } - - /** - * Add an array of agencies - * - * @param array $agencies List of agencies. - * @param int $publicationId Submission ID. - * @param bool $deleteFirst True iff existing agencies should be removed first. - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function scopeInsertAgencies( - Builder $query, - array $agencies, - int $publicationId, - bool $deleteFirst = true, - int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION - ): void - { - Repo::controlledVocab()->insertBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, - $agencies, - $assocType, - $publicationId, - $deleteFirst - ); - } -} diff --git a/classes/submission/SubmissionDisciplineVocab.php b/classes/submission/SubmissionDisciplineVocab.php deleted file mode 100644 index 42217ff74f0..00000000000 --- a/classes/submission/SubmissionDisciplineVocab.php +++ /dev/null @@ -1,92 +0,0 @@ -getBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, - $assocType, - $publicationId, - $locales - ); - } - - /** - * Get an array of all of the submission's disciplines - */ - public function scopeGetAllUniqueDisciplines(Builder $query): array - { - return Repo::controlledVocab()->getAllUniqueBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE - ); - } - - /** - * Add an array of disciplines - * - * @param array $disciplines - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function scopeInsertDisciplines( - Builder $query, - array $disciplines, - int $publicationId, - bool $deleteFirst = true, - int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION): void - { - Repo::controlledVocab()->insertBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, - $disciplines, - $assocType, - $publicationId, - $deleteFirst - ); - } -} diff --git a/classes/submission/SubmissionKeywordVocab.php b/classes/submission/SubmissionKeywordVocab.php deleted file mode 100644 index 6a371e9809e..00000000000 --- a/classes/submission/SubmissionKeywordVocab.php +++ /dev/null @@ -1,94 +0,0 @@ -getBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, - $assocType, - $publicationId, - $locales - ); - } - - /** - * Get an array of all of the submission's keywords - * - * @return array - */ - public function scopeGetAllUniqueKeywords(Builder $query): array - { - return Repo::controlledVocab()->getAllUniqueBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD - ); - } - - /** - * Add an array of keywords - * - * @param array $keywords - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function scopeInsertKeywords( - Builder $query, - array $keywords, - int $publicationId, - bool $deleteFirst = true, - int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION - ): void - { - Repo::controlledVocab()->insertBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, - $keywords, - $assocType, - $publicationId, - $deleteFirst - ); - } -} diff --git a/classes/submission/SubmissionSubjectVocab.php b/classes/submission/SubmissionSubjectVocab.php deleted file mode 100644 index 63bb8a09469..00000000000 --- a/classes/submission/SubmissionSubjectVocab.php +++ /dev/null @@ -1,90 +0,0 @@ -getBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, - $assocType, - $publicationId, - $locales - ); - } - - /** - * Get an array of all of the submission's Subjects - */ - public function scopeGetAllUniqueSubjects(Builder $query): array - { - return Repo::controlledVocab()->getAllUniqueBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT - ); - } - - /** - * Add an array of subjects - * - * @param array $subjects - * @param int $publicationId - * @param bool $deleteFirst - * @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213 - */ - public function scopeInsertSubjects( - Builder $query, - array $subjects, - int $publicationId, - bool $deleteFirst = true, - int $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION - ): void - { - Repo::controlledVocab()->insertBySymbolic( - static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, - $subjects, - $assocType, - $publicationId, - $deleteFirst - ); - } -} diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index eb7d47f506f..db84fff1e15 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -20,14 +20,11 @@ use APP\facades\Repo; use APP\publication\Publication; use PKP\citation\CitationDAO; +use PKP\controlledVocab\ControlledVocab; use PKP\db\DAORegistry; use PKP\filter\Filter; use PKP\filter\FilterGroup; use PKP\plugins\PluginRegistry; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionSubjectVocab; class NativeXmlPKPPublicationFilter extends NativeImportFilter { @@ -142,10 +139,9 @@ public function handleChildElement($n, $publication) if (in_array($n->tagName, $setterMappings)) { $publication->setData($n->tagName, $value, $locale); } elseif (isset($controlledVocabulariesMappings[$n->tagName])) { - $controlledVocabulariesModel = $controlledVocabulariesMappings[$n->tagName][0]; - $insertFunction = $controlledVocabulariesMappings[$n->tagName][1]; - + $symbolic = $controlledVocabulariesMappings[$n->tagName][0]; $controlledVocabulary = []; + for ($nc = $n->firstChild; $nc !== null; $nc = $nc->nextSibling) { if ($nc instanceof \DOMElement) { $controlledVocabulary[] = $nc->textContent; @@ -155,7 +151,13 @@ public function handleChildElement($n, $publication) $controlledVocabulariesValues = []; $controlledVocabulariesValues[$locale] = $controlledVocabulary; - $controlledVocabulariesModel::$insertFunction($controlledVocabulariesValues, $publication->getId(), false); + Repo::controlledVocab()->insertBySymbolic( + $symbolic, + $controlledVocabulariesValues, + Application::ASSOC_TYPE_PUBLICATION, + $publication->getId(), + false + ); $publicationNew = Repo::publication()->get($publication->getId()); $publication->setData($n->tagName, $publicationNew->getData($n->tagName)); @@ -319,10 +321,10 @@ public function _getLocalizedPublicationFields() public function _getControlledVocabulariesMappings() { return [ - 'keywords' => [SubmissionKeywordVocab::class, 'insertKeywords'], - 'agencies' => [SubmissionAgencyVocab::class, 'insertAgencies'], - 'disciplines' => [SubmissionDisciplineVocab::class, 'insertDisciplines'], - 'subjects' => [SubmissionSubjectVocab::class, 'insertSubjects'], + 'keywords' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD], + 'agencies' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY], + 'disciplines' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE], + 'subjects' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT], ]; } diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index c8937b63140..15ce3161529 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -17,9 +17,11 @@ namespace PKP\plugins\importexport\native\filter; use APP\core\Application; +use APP\facades\Repo; use APP\plugins\importexport\native\NativeImportExportDeployment; use APP\publication\Publication; use Exception; +use PKP\controlledVocab\ControlledVocab; use PKP\citation\CitationDAO; use PKP\db\DAORegistry; use PKP\filter\FilterGroup; @@ -28,10 +30,6 @@ use PKP\submission\PKPSubmission; use PKP\submission\Representation; use PKP\submission\RepresentationDAOInterface; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionSubjectVocab; class PKPPublicationNativeXmlFilter extends NativeExportFilter { @@ -220,10 +218,14 @@ public function addMetadata($doc, $entityNode, $entity) $supportedLocales = $deployment->getContext()->getSupportedFormLocales(); $controlledVocabulariesMapping = $this->_getControlledVocabulariesMappings(); foreach ($controlledVocabulariesMapping as $controlledVocabulariesNodeName => $mappings) { - $vocabModel = $mappings[0]; - $getFunction = $mappings[1]; - $controlledVocabularyNodeName = $mappings[2]; - $controlledVocabulary = $vocabModel::$getFunction($entity->getId(), $supportedLocales); + $symbolic = $mappings[0]; + $controlledVocabularyNodeName = $mappings[1]; + $controlledVocabulary = Repo::controlledVocab()->getBySymbolic( + $symbolic, + Application::ASSOC_TYPE_PUBLICATION, + $entity->getId(), + $supportedLocales + ); $this->addControlledVocabulary($doc, $entityNode, $controlledVocabulariesNodeName, $controlledVocabularyNodeName, $controlledVocabulary); } } @@ -308,10 +310,10 @@ public function addRepresentations($doc, $entityNode, $entity) public function _getControlledVocabulariesMappings() { return [ - 'keywords' => [SubmissionKeywordVocab::class, 'getKeywords', 'keyword'], - 'agencies' => [SubmissionAgencyVocab::class, 'getAgencies', 'agency'], - 'disciplines' => [SubmissionDisciplineVocab::class, 'getDisciplines', 'discipline'], - 'subjects' => [SubmissionSubjectVocab::class, 'getSubjects', 'subject'], + 'keywords' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 'keyword'], + 'agencies' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, 'agency'], + 'disciplines' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, 'discipline'], + 'subjects' => [ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, 'subject'], ]; } diff --git a/tests/classes/publication/PublicationTest.php b/tests/classes/publication/PublicationTest.php index 31c92cd8bff..7045dedcb60 100644 --- a/tests/classes/publication/PublicationTest.php +++ b/tests/classes/publication/PublicationTest.php @@ -21,10 +21,6 @@ use APP\publication\Publication; use PKP\citation\CitationDAO; use PKP\services\PKPSchemaService; -use PKP\submission\SubmissionAgencyVocab; -use PKP\submission\SubmissionDisciplineVocab; -use PKP\submission\SubmissionKeywordVocab; -use PKP\submission\SubmissionSubjectVocab; use PKP\tests\PKPTestCase; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index 7cef5d0bf30..3d586b99b8d 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -49,5 +49,10 @@ public function testValidatorControlledVocab() self::assertTrue($validator->isValid($controlledVocabEntryId1)); self::assertTrue($validator->isValid($controlledVocabEntryId2)); self::assertFalse($validator->isValid(3)); + + // Delete the test entried + $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId1); + $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId2); + $testControlledVocab->delete(); } } From 52c26cc46aa0049b38974de0f75f53e6c4734bb6 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 27 Aug 2024 09:58:26 +0600 Subject: [PATCH 08/30] pkp/pkp-lib#10292 fixing issue with vocab storing and some doc blocks added --- classes/controlledVocab/ControlledVocab.php | 66 +++++++++++++++------ classes/publication/DAO.php | 2 +- classes/user/interest/Repository.php | 4 +- classes/user/interest/UserInterest.php | 21 +++++++ 4 files changed, 73 insertions(+), 20 deletions(-) diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index c4bf848ebde..7d025119e1b 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -26,15 +26,34 @@ class ControlledVocab extends Model { use HasCamelCasing; + /** + * List of pre defined vocab symbolic as const in format of CONTROLLED_VOCAB_* + */ public const CONTROLLED_VOCAB_SUBMISSION_AGENCY = 'submissionAgency'; public const CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE = 'submissionDiscipline'; public const CONTROLLED_VOCAB_SUBMISSION_KEYWORD = 'submissionKeyword'; public const CONTROLLED_VOCAB_SUBMISSION_LANGUAGE = 'submissionLanguage'; public const CONTROLLED_VOCAB_SUBMISSION_SUBJECT = 'submissionSubject'; + /** + * The table associated with the model. + * + * @var string + */ protected $table = 'controlled_vocabs'; + + /** + * The primary key for the model. + * + * @var string + */ protected $primaryKey = 'controlled_vocab_id'; + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ protected $guarded = [ 'controlled_vocab_id', ]; @@ -46,22 +65,11 @@ class ControlledVocab extends Model */ public $timestamps = false; - public static function getDefinedVocabSymbolic(): array - { - return [ - static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, - static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, - static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, - static::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE, - static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, - ]; - } - - public static function hasDefinedVocabSymbolic(string $vocab): bool - { - return in_array($vocab, static::getDefinedVocabSymbolic()); - } - + /** + * Get the attributes that should be cast. + * + * @return array + */ protected function casts(): array { return [ @@ -82,6 +90,31 @@ protected function id(): Attribute ); } + /** + * Get the list of pre defined vocab symbolics + */ + public static function getDefinedVocabSymbolic(): array + { + return [ + static::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + static::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, + static::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + static::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE, + static::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, + ]; + } + + /** + * Check if a provided vocab symbolic defined in pre defined symbolic list + */ + public static function hasDefinedVocabSymbolic(string $vocab): bool + { + return in_array($vocab, static::getDefinedVocabSymbolic()); + } + + /** + * Get the locale field names for this controlled vocab + */ public function getLocaleFieldNames(): array { if (!$this->symbolic) { @@ -124,7 +157,6 @@ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Bu /** * Get a list of controlled vocabulary options. * - * @param string $settingName optional * @return array $controlledVocabEntryId => name */ public function enumerate(string $settingName = 'name'): array diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 31d874720eb..5fe580eed84 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -349,7 +349,7 @@ protected function setControlledVocab(Publication $publication) $publication->setData( 'keywords', Repo::controlledVocab()->getBySymbolic( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_PUBLICATION, $publication->getId() ) diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 8fb8c304696..9bc59ad7c4d 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -9,10 +9,10 @@ * * @class Repository * - * @brief + * @brief A repository to manage actions related to user interest */ - namespace PKP\user\interest; +namespace PKP\user\interest; use APP\facades\Repo; use PKP\db\DAORegistry; diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index e8c40e4fe04..b7d8595b77c 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -26,9 +26,25 @@ class UserInterest extends Model public const CONTROLLED_VOCAB_INTEREST = 'interest'; + /** + * The table associated with the model. + * + * @var string + */ protected $table = 'user_interests'; + + /** + * The primary key for the model. + * + * @var string + */ protected $primaryKey = 'user_interest_id'; + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ protected $guarded = [ 'user_interest_id', ]; @@ -40,6 +56,11 @@ class UserInterest extends Model */ public $timestamps = false; + /** + * Get the attributes that should be cast. + * + * @return array + */ protected function casts(): array { return [ From efbda26dc777389e07083176a56c7912e9ac051c Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Thu, 3 Oct 2024 14:22:16 +0600 Subject: [PATCH 09/30] pkp/pkp-lib#10292 added TODO to investigate the impact of merged issue #10423 PR --- classes/core/PKPApplication.php | 1 - classes/user/interest/Repository.php | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index e59da6c8a8e..5c26f740732 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -491,7 +491,6 @@ public function getDAOMap(): array 'SubmissionAgencyEntryDAO' => 'PKP\submission\SubmissionAgencyEntryDAO', 'SubmissionCommentDAO' => 'PKP\submission\SubmissionCommentDAO', 'SubmissionDisciplineEntryDAO' => 'PKP\submission\SubmissionDisciplineEntryDAO', - 'SubmissionKeywordDAO' => 'PKP\submission\SubmissionKeywordDAO', 'SubmissionKeywordEntryDAO' => 'PKP\submission\SubmissionKeywordEntryDAO', 'SubmissionSubjectEntryDAO' => 'PKP\submission\SubmissionSubjectEntryDAO', 'TemporaryFileDAO' => 'PKP\file\TemporaryFileDAO', diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 9bc59ad7c4d..5cb7661da0d 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -128,6 +128,7 @@ public function setUserInterests(array $interests, int $userId): void $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); } + // TODO: Investigate the impact of applied patch from https://github.com/pkp/pkp-lib/issues/10423 UserInterest::create([ 'userId' => $userId, 'controlledVocabEntryId' => $interestEntry->getId(), From e609b39a54cca815a89d5eb0ef3e93485ce63a26 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 7 Oct 2024 16:27:41 +0600 Subject: [PATCH 10/30] pkp/pkp-lib#10292 refactored entry dao to model class --- api/v1/vocabs/PKPVocabController.php | 21 +- classes/controlledVocab/ControlledVocab.php | 69 ++-- .../controlledVocab/ControlledVocabEntry.php | 204 ++++++++---- .../ControlledVocabEntryDAO.php | 294 ------------------ classes/controlledVocab/Repository.php | 137 ++++---- classes/core/PKPApplication.php | 6 - classes/core/traits/ModelWithSettings.php | 9 +- classes/metadata/MetadataProperty.php | 15 +- classes/services/PKPSchemaService.php | 2 +- classes/submission/SubmissionAgency.php | 56 ---- .../submission/SubmissionAgencyEntryDAO.php | 61 ---- classes/submission/SubmissionDiscipline.php | 56 ---- .../SubmissionDisciplineEntryDAO.php | 61 ---- classes/submission/SubmissionKeyword.php | 56 ---- .../submission/SubmissionKeywordEntryDAO.php | 62 ---- classes/submission/SubmissionLanguage.php | 60 ---- .../submission/SubmissionLanguageEntryDAO.php | 64 ---- classes/submission/SubmissionSubject.php | 59 ---- .../submission/SubmissionSubjectEntryDAO.php | 61 ---- classes/user/InterestEntry.php | 46 --- classes/user/InterestEntryDAO.php | 100 ------ classes/user/InterestManager.php | 53 ++-- classes/user/interest/Repository.php | 158 ++++------ classes/user/interest/UserInterest.php | 56 ++-- classes/user/maps/Schema.php | 19 +- .../validation/ValidatorControlledVocab.php | 6 +- .../FormValidatorControlledVocabTest.php | 49 +-- .../classes/metadata/MetadataPropertyTest.php | 41 +-- .../ValidatorControlledVocabTest.php | 48 +-- 29 files changed, 516 insertions(+), 1413 deletions(-) delete mode 100644 classes/controlledVocab/ControlledVocabEntryDAO.php delete mode 100644 classes/submission/SubmissionAgency.php delete mode 100644 classes/submission/SubmissionAgencyEntryDAO.php delete mode 100644 classes/submission/SubmissionDiscipline.php delete mode 100644 classes/submission/SubmissionDisciplineEntryDAO.php delete mode 100644 classes/submission/SubmissionKeyword.php delete mode 100644 classes/submission/SubmissionKeywordEntryDAO.php delete mode 100644 classes/submission/SubmissionLanguage.php delete mode 100644 classes/submission/SubmissionLanguageEntryDAO.php delete mode 100644 classes/submission/SubmissionSubject.php delete mode 100644 classes/submission/SubmissionSubjectEntryDAO.php delete mode 100644 classes/user/InterestEntry.php delete mode 100644 classes/user/InterestEntryDAO.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 3cb342baec7..511baa78d02 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -24,6 +24,7 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Route; use PKP\controlledVocab\ControlledVocab; +use PKP\controlledVocab\ControlledVocabEntry; use PKP\core\PKPBaseController; use PKP\core\PKPRequest; use PKP\facades\Locale; @@ -101,7 +102,12 @@ public function getMany(Request $illuminateRequest): JsonResponse $vocab = $requestParams['vocab'] ?? ''; $locale = $requestParams['locale'] ?? Locale::getLocale(); $term = $requestParams['term'] ?? null; - $locales = array_merge($context->getSupportedSubmissionMetadataLocales(), isset($requestParams['submissionId']) ? Repo::submission()->get((int) $requestParams['submissionId'])?->getPublicationLanguages() ?? [] : []); + $locales = array_merge( + $context->getSupportedSubmissionMetadataLocales(), + isset($requestParams['submissionId']) + ? (Repo::submission()->get((int) $requestParams['submissionId'])?->getPublicationLanguages() ?? []) + : [] + ); if (!in_array($locale, $locales)) { return response()->json([ @@ -110,9 +116,14 @@ public function getMany(Request $illuminateRequest): JsonResponse } if (ControlledVocab::hasDefinedVocabSymbolic($vocab)) { - /** @var \PKP\controlledVocab\ControlledVocabEntryDAO $entryDao */ - $entryDao = Repo::controlledVocab()->getEntryDaoBySymbolic($vocab); - $entries = $entryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray(); + $entries = ControlledVocabEntry::query() + ->whereHas( + "controlledVocab", + fn($query) => $query->withSymbolic($vocab)->withContextId($context->getId()) + ) + ->withLocale($locale) + ->when($term, fn ($query) => $query->withSetting($vocab, $term)) + ->get(); } else { $entries = []; Hook::call('API::vocabs::getMany', [$vocab, &$entries, $illuminateRequest, response(), $request]); @@ -120,7 +131,7 @@ public function getMany(Request $illuminateRequest): JsonResponse $data = []; foreach ($entries as $entry) { - $data[] = $entry->getData($vocab, $locale); + $data[] = $entry->getLocalizedData($vocab, $locale); } $data = array_values(array_unique($data)); diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index 7d025119e1b..5a11c9bf372 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -1,7 +1,7 @@ |bool + * @copydoc \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::$guarded */ protected $guarded = [ 'controlled_vocab_id', ]; /** - * Indicates if the model should be timestamped. - * - * @var bool + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasTimestamps::$timestamps */ public $timestamps = false; /** - * Get the attributes that should be cast. - * - * @return array + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasAttributes::casts */ protected function casts(): array { @@ -87,7 +79,7 @@ protected function id(): Attribute return Attribute::make( get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, set: fn($value) => [$this->primaryKey => $value], - ); + )->shouldCache(); } /** @@ -112,6 +104,7 @@ public static function hasDefinedVocabSymbolic(string $vocab): bool return in_array($vocab, static::getDefinedVocabSymbolic()); } + // TODO: Investigate if this is necessary anymore /** * Get the locale field names for this controlled vocab */ @@ -126,6 +119,7 @@ public function getLocaleFieldNames(): array : []; } + // TODO: Investigate if this is necessary anymore /** * Compatibility function for including note IDs in grids. * @@ -137,7 +131,15 @@ public function getId(): int } /** - * Scope a query to only include notes with a specific user ID. + * Get all controlled vocab entries for this controlled vocab + */ + public function controlledVocabEntries(): HasMany + { + return $this->hasMany(ControlledVocabEntry::class, 'controlled_vocab_id', 'controlled_vocab_id'); + } + + /** + * Scope a query to only include vocabs with a specific symbolic. */ public function scopeWithSymbolic(Builder $query, string $symbolic): Builder { @@ -145,7 +147,7 @@ public function scopeWithSymbolic(Builder $query, string $symbolic): Builder } /** - * Scope a query to only include notes with a specific assoc type and assoc ID. + * Scope a query to only include vocabs with a specific assoc type and assoc ID. */ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Builder { @@ -154,13 +156,40 @@ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Bu ->where('assoc_id', $assocId); } + /** + * Scope a query to only include vocabs associated with given context id + */ + public function scopeWithContextId(Builder $query, int $contextId): Builder + { + return $query + ->where( + fn ($query) => $query + ->select('context_id') + ->from('submissions') + ->whereColumn( + DB::raw( + "(SELECT publications.submission_id + FROM publications + INNER JOIN {$this->table} + ON publications.publication_id = {$this->table}.assoc_id + LIMIT 1)" + ), + '=', + 'submissions.submission_id' + ), + $contextId + ); + } + /** * Get a list of controlled vocabulary options. * * @return array $controlledVocabEntryId => name */ - public function enumerate(string $settingName = 'name'): array + public function enumerate(?string $settingName = null): array { + $settingName ??= $this->symbolic; + return DB::table('controlled_vocab_entries AS e') ->leftJoin( 'controlled_vocab_entry_settings AS l', diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 92a7703057b..8cced60bf01 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -3,101 +3,201 @@ /** * @file classes/controlledVocab/ControlledVocabEntry.php * - * Copyright (c) 2014-2021 Simon Fraser University - * Copyright (c) 2000-2021 John Willinsky + * 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 ControlledVocabEntry * - * @ingroup controlled_vocabs - * - * @see ControlledVocabEntryDAO - * - * @brief Basic class describing a controlled vocab. + * @brief ControlledVocabEntry model class */ namespace PKP\controlledVocab; -class ControlledVocabEntry extends \PKP\core\DataObject +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\DB; +use PKP\user\interest\UserInterest; +use PKP\core\traits\ModelWithSettings; +use Illuminate\Database\Eloquent\Model; +use PKP\controlledVocab\ControlledVocab; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +class ControlledVocabEntry extends Model { - // - // Get/set methods - // + use ModelWithSettings; + + /** + * @copydoc \Illuminate\Database\Eloquent\Model::$table + */ + protected $table = 'controlled_vocab_entries'; + + /** + * @copydoc \Illuminate\Database\Eloquent\Model::$primaryKey + */ + protected $primaryKey = 'controlled_vocab_entry_id'; + + // TODO: Investigate why defining any guarded props causing no data to store in settings table + /** + * @copydoc \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::$guarded + */ + protected $guarded = [ + // 'id', + ]; + + /** + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasTimestamps::$timestamps + */ + public $timestamps = false; /** - * Get the ID of the controlled vocab. - * - * @return int + * @inheritDoc */ - public function getControlledVocabId() + public function getSettingsTable(): string { - return $this->getData('controlledVocabId'); + return 'controlled_vocab_entry_settings'; } /** - * Set the ID of the controlled vocab. - * - * @param int $controlledVocabId + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasAttributes::casts */ - public function setControlledVocabId($controlledVocabId) + protected function casts(): array { - $this->setData('controlledVocabId', $controlledVocabId); + return [ + 'controlled_vocab_entry_id' => 'string', + 'controlled_vocab_id' => 'int', + 'seq' => 'float', + ]; } /** - * Get sequence number. - * - * @return float + * @inheritDoc */ - public function getSequence() + public static function getSchemaName(): ?string { - return $this->getData('sequence'); + return null; } /** - * Set sequence number. - * - * @param float $sequence + * @inheritDoc */ - public function setSequence($sequence) + public function getMultilingualProps(): array { - $this->setData('sequence', $sequence); + return array_merge( + $this->multilingualProps, + ControlledVocab::getDefinedVocabSymbolic(), + Arr::wrap(UserInterest::CONTROLLED_VOCAB_INTEREST) + ); } /** - * Get the localized name. - * - * @return string + * @inheritDoc */ - public function getLocalizedName() + public function getSettings(): array { - return $this->getLocalizedData('name'); + return array_merge( + $this->settings, + ControlledVocab::getDefinedVocabSymbolic(), + Arr::wrap(UserInterest::CONTROLLED_VOCAB_INTEREST) + ); } /** - * Get the name of the controlled vocabulary entry. - * - * @param string $locale - * - * @return string + * Accessor and Mutator for primary key => id */ - public function getName($locale) + protected function id(): Attribute { - return $this->getData('name', $locale); + return Attribute::make( + get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn($value) => [$this->primaryKey => $value], + )->shouldCache(); } /** - * Set the name of the controlled vocabulary entry. - * - * @param string $name - * @param string $locale + * The controlled vocab associated with this controlled vocab entry */ - public function setName($name, $locale) + public function controlledVocab(): BelongsTo { - $this->setData('name', $name, $locale); + return $this->belongsTo(ControlledVocab::class, 'controlled_vocab_id', 'controlled_vocab_id'); } -} -if (!PKP_STRICT_MODE) { - class_alias('\PKP\controlledVocab\ControlledVocabEntry', '\ControlledVocabEntry'); + /** + * The user interest associated with this controlled vocab entry + */ + public function userInterest(): BelongsTo + { + return $this->belongsTo(UserInterest::class, 'controlled_vocab_entry_id', 'controlled_vocab_entry_id'); + } + + /** + * Scope a query to only include entries for a specific controlled vocab id + */ + public function scopeWithControlledVocabId(Builder $query, int $controlledVocabId): Builder + { + return $query->where('controlled_vocab_id', $controlledVocabId); + } + + /** + * Scope a query to only include entries for a specific locale/s + */ + public function scopeWithLocale(Builder $query, string|array $locale): Builder + { + if (is_array($locale)) { + return $query->whereIn( + DB::raw( + "(SELECT locale + FROM {$this->getSettingsTable()} + WHERE {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} + LIMIT 1)" + ), + $locale + ); + } + + return $query->where( + fn ($query) => $query + ->select("locale") + ->from("{$this->getSettingsTable()}") + ->whereColumn( + "{$this->getSettingsTable()}.{$this->primaryKey}", + "{$this->table}.{$this->primaryKey}" + ) + ->limit(1), + $locale + ); + } + + /** + * Scope a query to only include entries for a specific setting name and value/s + */ + public function scopeWithSetting(Builder $query, string $settingName, string|array $settingValue, bool $partial = true): Builder + { + if (is_array($settingValue)) { + return $query->whereIn( + DB::raw( + "(SELECT setting_value + FROM {$this->getSettingsTable()} + WHERE setting_name = '{$settingName}' + AND {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} + LIMIT 1)" + ), + $settingValue + ); + } + + return $query->where( + fn ($query) => $query + ->select('setting_value') + ->from("{$this->getSettingsTable()}") + ->where('setting_name', $settingName) + ->whereColumn( + "{$this->getSettingsTable()}.{$this->primaryKey}", + "{$this->table}.{$this->primaryKey}" + ) + ->limit(1), + ($partial ? 'LIKE' : '='), + ($partial ? "%{$settingValue}%" : $settingValue) + ); + } } diff --git a/classes/controlledVocab/ControlledVocabEntryDAO.php b/classes/controlledVocab/ControlledVocabEntryDAO.php deleted file mode 100644 index 26dc3afa7eb..00000000000 --- a/classes/controlledVocab/ControlledVocabEntryDAO.php +++ /dev/null @@ -1,294 +0,0 @@ -retrieve( - 'SELECT * FROM controlled_vocab_entries WHERE controlled_vocab_entry_id = ?' . - (!empty($controlledVocabId) ? ' AND controlled_vocab_id = ?' : ''), - $params - ); - $row = $result->current(); - return $row ? $this->_fromRow((array) $row) : null; - } - - /** - * Retrieve a controlled vocab entry by resolving one of its settings - * to the corresponding entry id. - * - * @param string $settingValue the setting value to be searched for - * @param string $symbolic the vocabulary to be searched, identified by its symbolic name - * @param int $assocType - * @param int $assocId - * @param string $settingName the setting to be searched - * @param string $locale - * - * @return ControlledVocabEntry - */ - public function getBySetting($settingValue, $symbolic, $assocType = 0, $assocId = 0, $settingName = 'name', $locale = '') - { - $result = $this->retrieve( - 'SELECT cve.* - FROM controlled_vocabs cv - INNER JOIN controlled_vocab_entries cve ON cv.controlled_vocab_id = cve.controlled_vocab_id - INNER JOIN controlled_vocab_entry_settings cves ON cve.controlled_vocab_entry_id = cves.controlled_vocab_entry_id - WHERE cves.setting_name = ? AND - cves.locale = ? AND - cves.setting_value = ? AND - cv.symbolic = ? AND - cv.assoc_type = ? AND - cv.assoc_id = ?', - [$settingName, $locale, $settingValue, $symbolic, $assocType, $assocId] - ); - $row = $result->current(); - return $row ? $this->_fromRow((array) $row) : null; - } - - /** - * Construct a new data object corresponding to this DAO. - * - * @return ControlledVocabEntry - */ - public function newDataObject() - { - return new ControlledVocabEntry(); - } - - /** - * Internal function to return a ControlledVocabEntry object from a - * row. - * - * @param array $row - * - * @return ControlledVocabEntry - */ - public function _fromRow($row) - { - $controlledVocabEntry = $this->newDataObject(); - $controlledVocabEntry->setControlledVocabId($row['controlled_vocab_id']); - $controlledVocabEntry->setId($row['controlled_vocab_entry_id']); - $controlledVocabEntry->setSequence($row['seq']); - - $this->getDataObjectSettings('controlled_vocab_entry_settings', 'controlled_vocab_entry_id', $row['controlled_vocab_entry_id'], $controlledVocabEntry); - - return $controlledVocabEntry; - } - - /** - * Get the list of fields for which data can be localized. - */ - public function getLocaleFieldNames(): array - { - return ['name']; - } - - /** - * Update the localized fields for this table - * - * @param object $controlledVocabEntry - */ - public function updateLocaleFields($controlledVocabEntry) - { - $this->updateDataObjectSettings('controlled_vocab_entry_settings', $controlledVocabEntry, [ - 'controlled_vocab_entry_id' => $controlledVocabEntry->getId() - ]); - } - - /** - * Insert a new ControlledVocabEntry. - * - * @param ControlledVocabEntry $controlledVocabEntry - * - * @return int Inserted controlled vocabulary entry ID - */ - public function insertObject($controlledVocabEntry) - { - $this->update( - 'INSERT INTO controlled_vocab_entries (controlled_vocab_id, seq) - VALUES (?, ?)', - [ - (int) $controlledVocabEntry->getControlledVocabId(), - (float) $controlledVocabEntry->getSequence() - ] - ); - $controlledVocabEntry->setId($this->getInsertId()); - $this->updateLocaleFields($controlledVocabEntry); - return (int)$controlledVocabEntry->getId(); - } - - /** - * Delete a controlled vocab entry. - * - * @param ControlledVocabEntry $controlledVocabEntry - */ - public function deleteObject($controlledVocabEntry) - { - $this->deleteObjectById($controlledVocabEntry->getId()); - } - - /** - * Delete a controlled vocab entry by controlled vocab entry ID. - * - * @param int $controlledVocabEntryId - */ - public function deleteObjectById($controlledVocabEntryId) - { - $this->update('DELETE FROM controlled_vocab_entry_settings WHERE controlled_vocab_entry_id = ?', [(int) $controlledVocabEntryId]); - $this->update('DELETE FROM controlled_vocab_entries WHERE controlled_vocab_entry_id = ?', [(int) $controlledVocabEntryId]); - } - - /** - * Retrieve an iterator of controlled vocabulary entries matching a - * particular controlled vocabulary ID. - * - * @param int $controlledVocabId - * @param ?DBResultRange $rangeInfo - * @param null|mixed $filter - * - * @return DAOResultFactory Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - $params = [(int) $controlledVocabId]; - if (!empty($filter)) { - $params[] = "%{$filter}%"; - } - - $result = $this->retrieveRange( - 'SELECT * - FROM controlled_vocab_entries cve ' . - (!empty($filter) ? 'INNER JOIN controlled_vocab_entry_settings cves ON cve.controlled_vocab_entry_id = cves.controlled_vocab_entry_id ' : '') . - 'WHERE controlled_vocab_id = ? ' . - (!empty($filter) ? 'AND cves.setting_value LIKE ? ' : '') . - 'ORDER BY seq', - $params, - $rangeInfo - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } - - /** - * Retrieve an array of controlled vocab entries that exist for a given context - * (assigned to at least one submission in that context) and which match the - * requested symbolic (eg - keywords/subjects) - * - * @param string $symbolic One of the CONTROLLED_VOCAB_* constants - * @param int $contextId - * @param string $locale - * - * @return DAOResultFactory - */ - public function getByContextId($symbolic, int $contextId, $locale, ?string $term = null) - { - $params = [ - $symbolic, - PKPApplication::ASSOC_TYPE_PUBLICATION, - $contextId, - $locale - ]; - $words = array_map(fn (string $word) => '%' . addcslashes($word, '%_') . '%', preg_split('/\s+/u', trim($term ?? ''))); - - $termFilter = ''; - if (count($words)) { - array_push($params, ...$words); - $condition = 'cves.setting_value LIKE ?'; - $termFilter = " AND ({$condition}" . str_repeat(" OR {$condition}", count($words) - 1) . ')'; - } - $result = $this->retrieve( - "SELECT cve.* - FROM controlled_vocab_entries AS cve - INNER JOIN controlled_vocabs AS cv ON cv.controlled_vocab_id = cve.controlled_vocab_id - INNER JOIN controlled_vocab_entry_settings AS cves ON cves.controlled_vocab_entry_id = cve.controlled_vocab_entry_id - INNER JOIN publications as p ON p.publication_id = cv.assoc_id - INNER JOIN submissions AS s ON s.submission_id = p.submission_id - WHERE - cv.symbolic = ? - AND cv.assoc_type = ? - AND s.context_id = ? - AND cves.locale = ? - {$termFilter} - ORDER BY - cves.setting_value", - $params - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } - - /** - * Update an existing review form element. - * - * @param ControlledVocabEntry $controlledVocabEntry - */ - public function updateObject($controlledVocabEntry) - { - $this->update( - 'UPDATE controlled_vocab_entries - SET controlled_vocab_id = ?, - seq = ? - WHERE controlled_vocab_entry_id = ?', - [ - (int) $controlledVocabEntry->getControlledVocabId(), - (float) $controlledVocabEntry->getSequence(), - (int) $controlledVocabEntry->getId() - ] - ); - $this->updateLocaleFields($controlledVocabEntry); - } - - /** - * Sequentially renumber entries in their sequence order. - * - * @param int $controlledVocabId Controlled vocabulary ID - */ - public function resequence($controlledVocabId) - { - $result = $this->retrieve('SELECT controlled_vocab_entry_id FROM controlled_vocab_entries WHERE controlled_vocab_id = ? ORDER BY seq', [(int) $controlledVocabId]); - - for ($i = 1; $row = $result->current(); $i++) { - $this->update('UPDATE controlled_vocab_entries SET seq = ? WHERE controlled_vocab_entry_id = ?', [(int) $i, (int) $row->controlled_vocab_entry_id]); - $result->next(); - } - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\controlledVocab\ControlledVocabEntryDAO', '\ControlledVocabEntryDAO'); -} diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 31a546853c6..bbaf23c1c7f 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -14,10 +14,11 @@ namespace PKP\controlledVocab; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; -use PKP\db\DAORegistry; use PKP\controlledVocab\ControlledVocab; -use PKP\controlledVocab\ControlledVocabEntryDAO; +use PKP\controlledVocab\ControlledVocabEntry; +use Throwable; class Repository { @@ -26,7 +27,8 @@ class Repository */ public function build(string $symbolic, int $assocType = 0, int $assocId = 0): ControlledVocab { - return ControlledVocab::withSymbolic($symbolic) + return ControlledVocab::query() + ->withSymbolic($symbolic) ->withAssoc($assocType, $assocId) ->firstOr(fn() => ControlledVocab::create([ 'assocType' => $assocType, @@ -35,28 +37,6 @@ public function build(string $symbolic, int $assocType = 0, int $assocId = 0): C ])); } - /** - * Return the Controlled Vocab Entry DAO for this Controlled Vocab. - * Can be subclassed to provide extended DAOs. - * - * Will be removed once the eloquent based settings table relations task completes. - */ - public function getEntryDAO(): ControlledVocabEntryDAO - { - return DAORegistry::getDAO('ControlledVocabEntryDAO'); - } - - /** - * Return the extended Controlled Vocab Entry DAO. - * Can be subclassed to provide extended DAOs. - * - * Will be removed once the eloquent based settings table relations task completes. - */ - public function getEntryDaoBySymbolic(string $symbolic): ControlledVocabEntryDAO - { - return DAORegistry::getDAO(ucfirst($symbolic) . 'EntryDAO'); - } - /** * Get localized entry data */ @@ -64,30 +44,24 @@ public function getBySymbolic( string $symbolic, int $assocType, int $assocId, - array $locales = [], - ?string $entryDaoClassName = null + array $locales = [] ): array { $result = []; - $controlledVocab = $this->build($symbolic, $assocType, $assocId); - - /** @var ControlledVocabEntryDAO $entryDao */ - $entryDao = $entryDaoClassName - ? DAORegistry::getDAO($entryDaoClassName) - : $this->getEntryDaoBySymbolic($symbolic); - - $controlledVocabEntries = $entryDao->getByControlledVocabId($controlledVocab->id); - - while ($vocabEntry = $controlledVocabEntries->next()) { - $vocabs = $vocabEntry->getData($symbolic); - foreach ($vocabs as $locale => $value) { - if (empty($locales) || in_array($locale, $locales)) { + ControlledVocabEntry::query() + ->whereHas( + "controlledVocab", + fn($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) + ) + ->withLocale($locales) + ->get() + ->each(function ($entry) use (&$result, $symbolic) { + foreach ($entry->{$symbolic} as $locale => $value) { $result[$locale][] = $value; } - } - } - + }); + return $result; } @@ -114,37 +88,62 @@ public function insertBySymbolic( int $assocType, int $assocId, bool $deleteFirst = true, - ?string $entryDaoClassName = null - ): void + ): bool { - /** @var ControlledVocabEntryDAO $entryDao */ - $entryDao = $entryDaoClassName - ? DAORegistry::getDAO($entryDaoClassName) - : $this->getEntryDaoBySymbolic($symbolic); + $controlledVocab = $this->build($symbolic, $assocType, $assocId); + $controlledVocab->load('controlledVocabEntries'); - $currentControlledVocab = $this->build($symbolic, $assocType, $assocId); + try { - if ($deleteFirst) { - collect($currentControlledVocab->enumerate( $symbolic)) - ->keys() - ->each(fn (int $id) => $entryDao->deleteObjectById($id)); - } + DB::beginTransaction(); - if (is_array($vocabs)) { // localized, array of arrays - foreach ($vocabs as $locale => $list) { - if (is_array($list)) { - $list = array_unique($list); // Remove any duplicate keywords - $i = 1; - foreach ($list as $vocab) { - $vocabEntry = $entryDao->newDataObject(); - $vocabEntry->setControlledVocabId($currentControlledVocab->id); - $vocabEntry->setData($symbolic, $vocab, $locale); - $vocabEntry->setSequence($i); - $i++; - $entryDao->insertObject($vocabEntry); - } - } + if ($deleteFirst) { + ControlledVocabEntry::whereIn( + 'id', + $controlledVocab->controlledVocabEntries->pluck('id')->toArray() + )->delete(); } + + collect($vocabs) + ->each( + fn (array|string $entries, string $locale) => collect(Arr::wrap($entries)) + ->each( + fn (string $vocab, $seq = 1) => + ControlledVocabEntry::create([ + 'controlledVocabId' => $controlledVocab->id, + 'seq' => $seq, + "{$symbolic}" => [ + $locale => $vocab + ], + ]) + ) + ); + + // TODO: Should Resequence? + + DB::commit(); + + return true; + + } catch (Throwable $exception) { + + DB::rollBack(); } + + return false; + } + + /** + * Resequence controlled vocab entries for a given controlled vocab id + */ + public function resequence(int $controlledVocabId): void + { + ControlledVocabEntry::query() + ->withControlledVocabId($controlledVocabId) + ->each( + fn ($controlledVocabEntry, $seq = 1) => $controlledVocabEntry->update([ + 'seq' => $seq, + ]) + ); } } diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index 5c26f740732..2d8460cf04c 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -464,13 +464,11 @@ public function getDAOMap(): array return [ 'AnnouncementTypeDAO' => 'PKP\announcement\AnnouncementTypeDAO', 'CitationDAO' => 'PKP\citation\CitationDAO', - 'ControlledVocabEntryDAO' => 'PKP\controlledVocab\ControlledVocabEntryDAO', 'DataObjectTombstoneDAO' => 'PKP\tombstone\DataObjectTombstoneDAO', 'DataObjectTombstoneSettingsDAO' => 'PKP\tombstone\DataObjectTombstoneSettingsDAO', 'FilterDAO' => 'PKP\filter\FilterDAO', 'FilterGroupDAO' => 'PKP\filter\FilterGroupDAO', 'GenreDAO' => 'PKP\submission\GenreDAO', - 'InterestEntryDAO' => 'PKP\user\InterestEntryDAO', 'LibraryFileDAO' => 'PKP\context\LibraryFileDAO', 'NavigationMenuDAO' => 'PKP\navigationMenu\NavigationMenuDAO', 'NavigationMenuItemDAO' => 'PKP\navigationMenu\NavigationMenuItemDAO', @@ -488,11 +486,7 @@ public function getDAOMap(): array 'RoleDAO' => 'PKP\security\RoleDAO', 'SiteDAO' => 'PKP\site\SiteDAO', 'SubEditorsDAO' => 'PKP\context\SubEditorsDAO', - 'SubmissionAgencyEntryDAO' => 'PKP\submission\SubmissionAgencyEntryDAO', 'SubmissionCommentDAO' => 'PKP\submission\SubmissionCommentDAO', - 'SubmissionDisciplineEntryDAO' => 'PKP\submission\SubmissionDisciplineEntryDAO', - 'SubmissionKeywordEntryDAO' => 'PKP\submission\SubmissionKeywordEntryDAO', - 'SubmissionSubjectEntryDAO' => 'PKP\submission\SubmissionSubjectEntryDAO', 'TemporaryFileDAO' => 'PKP\file\TemporaryFileDAO', 'TemporaryInstitutionsDAO' => 'PKP\statistics\TemporaryInstitutionsDAO', 'VersionDAO' => 'PKP\site\VersionDAO', diff --git a/classes/core/traits/ModelWithSettings.php b/classes/core/traits/ModelWithSettings.php index d077cf73bb3..a63c1096447 100644 --- a/classes/core/traits/ModelWithSettings.php +++ b/classes/core/traits/ModelWithSettings.php @@ -65,8 +65,13 @@ abstract protected function ensureCastsAreStringValues($casts); public function __construct(array $attributes = []) { parent::__construct($attributes); + if (static::getSchemaName()) { $this->setSchemaData(); + } else { + if (!empty($this->fillable)) { + $this->mergeFillable(array_merge($this->getSettings(), $this->getMultilingualProps())); + } } } @@ -135,11 +140,11 @@ protected function setSchemaData(): void $this->multilingualProps = array_merge($this->getMultilingualProps(), $schemaService->getMultilingualProps($this->getSchemaName())); $writableProps = $schemaService->groupPropsByOrigin($this->getSchemaName(), true); - $this->fillable = array_merge( + $this->fillable = array_values(array_unique(array_merge( $writableProps[Schema::ATTRIBUTE_ORIGIN_SETTINGS], $writableProps[Schema::ATTRIBUTE_ORIGIN_MAIN], $this->fillable, - ); + ))); } /** diff --git a/classes/metadata/MetadataProperty.php b/classes/metadata/MetadataProperty.php index 64426bd3ac2..873bda81603 100644 --- a/classes/metadata/MetadataProperty.php +++ b/classes/metadata/MetadataProperty.php @@ -34,8 +34,8 @@ namespace PKP\metadata; use InvalidArgumentException; +use PKP\controlledVocab\ControlledVocabEntry; use PKP\core\PKPString; -use PKP\db\DAORegistry; use PKP\validation\ValidatorControlledVocab; use PKP\validation\ValidatorFactory; @@ -423,8 +423,17 @@ public function isValid($value, $locale = null) if (is_string($value)) { // Try to translate the string value into a controlled vocab entry - $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); /** @var ControlledVocabEntryDAO $controlledVocabEntryDao */ - if (!is_null($controlledVocabEntryDao->getBySetting($value, $symbolic, $assocType, $assocId, 'name', $locale))) { + $entry = ControlledVocabEntry::query() + ->whereHas( + 'controlledVocab', + fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) + ) + ->withLocale($locale) + // TODO: Investigate if this need to be 'name' or $symbolic for settingName + ->withSetting('name', $value) + ->first(); + + if (!is_null($entry)) { // The string was successfully translated so mark it as "valid". return [self::METADATA_PROPERTY_TYPE_VOCABULARY => $allowedTypeParam]; } diff --git a/classes/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php index 01c71ac538f..f641b8564ba 100644 --- a/classes/services/PKPSchemaService.php +++ b/classes/services/PKPSchemaService.php @@ -272,7 +272,7 @@ public function groupPropsByOrigin(string $schemaName, bool $excludeReadOnly = f } // Exclude readonly if specified - if ($excludeReadOnly && !empty($propSchema->readOnly)) { + if ($excludeReadOnly && !empty($propSchema->readOnly) && $propSchema->readOnly) { continue; } diff --git a/classes/submission/SubmissionAgency.php b/classes/submission/SubmissionAgency.php deleted file mode 100644 index 25be6b8da18..00000000000 --- a/classes/submission/SubmissionAgency.php +++ /dev/null @@ -1,56 +0,0 @@ -getData('submissionAgency'); - } - - /** - * Set the agency text - * - * @param string $agency - * @param string $locale - */ - public function setAgency($agency, $locale) - { - $this->setData('submissionAgency', $agency, $locale); - } - - public function getLocaleMetadataFieldNames(): array - { - return ['submissionAgency']; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionAgency', '\SubmissionAgency'); -} diff --git a/classes/submission/SubmissionAgencyEntryDAO.php b/classes/submission/SubmissionAgencyEntryDAO.php deleted file mode 100644 index 2445f6b75a1..00000000000 --- a/classes/submission/SubmissionAgencyEntryDAO.php +++ /dev/null @@ -1,61 +0,0 @@ - Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - assert($filter == null); // Parent class supports this, but this class does not - $result = $this->retrieveRange( - 'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq', - [(int) $controlledVocabId], - $rangeInfo - ); - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionAgencyEntryDAO', '\SubmissionAgencyEntryDAO'); -} diff --git a/classes/submission/SubmissionDiscipline.php b/classes/submission/SubmissionDiscipline.php deleted file mode 100644 index bb1b80eef6b..00000000000 --- a/classes/submission/SubmissionDiscipline.php +++ /dev/null @@ -1,56 +0,0 @@ -getData('submissionDiscipline'); - } - - /** - * Set the discipline text - * - * @param string $discipline - * @param string $locale - */ - public function setDiscipline($discipline, $locale) - { - $this->setData('submissionDiscipline', $discipline, $locale); - } - - public function getLocaleMetadataFieldNames(): array - { - return ['submissionDiscipline']; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionDiscipline', '\SubmissionDiscipline'); -} diff --git a/classes/submission/SubmissionDisciplineEntryDAO.php b/classes/submission/SubmissionDisciplineEntryDAO.php deleted file mode 100644 index 3fd32cea69a..00000000000 --- a/classes/submission/SubmissionDisciplineEntryDAO.php +++ /dev/null @@ -1,61 +0,0 @@ - matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - assert($filter == null); // Parent class supports this, but this class does not - $result = $this->retrieveRange( - 'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq', - [(int) $controlledVocabId], - $rangeInfo - ); - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionDisciplineEntryDAO', '\SubmissionDisciplineEntryDAO'); -} diff --git a/classes/submission/SubmissionKeyword.php b/classes/submission/SubmissionKeyword.php deleted file mode 100644 index a824c97b425..00000000000 --- a/classes/submission/SubmissionKeyword.php +++ /dev/null @@ -1,56 +0,0 @@ -getData('submissionKeyword'); - } - - /** - * Set the keyword text - * - * @param string $keyword - * @param string $locale - */ - public function setKeyword($keyword, $locale) - { - $this->setData('submissionKeyword', $keyword, $locale); - } - - public function getLocaleMetadataFieldNames(): array - { - return ['submissionKeyword']; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionKeyword', '\SubmissionKeyword'); -} diff --git a/classes/submission/SubmissionKeywordEntryDAO.php b/classes/submission/SubmissionKeywordEntryDAO.php deleted file mode 100644 index 5b46898f62c..00000000000 --- a/classes/submission/SubmissionKeywordEntryDAO.php +++ /dev/null @@ -1,62 +0,0 @@ - Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - assert($filter == null); // Parent class supports this, but this class does not - $result = $this->retrieveRange( - 'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq', - [(int) $controlledVocabId], - $rangeInfo - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionKeywordEntryDAO', '\SubmissionKeywordEntryDAO'); -} diff --git a/classes/submission/SubmissionLanguage.php b/classes/submission/SubmissionLanguage.php deleted file mode 100644 index ed4496ed02f..00000000000 --- a/classes/submission/SubmissionLanguage.php +++ /dev/null @@ -1,60 +0,0 @@ -getData('submissionLanguage'); - } - - /** - * Set the language text - * - * @param string $language - * @param string $locale - */ - public function setLanguage($language, $locale) - { - $this->setData('submissionLanguage', $language, $locale); - } - - /** - * @copydoc \PKP\controlledVocab\ControlledVocabEntry::getLocaleMetadataFieldNames() - */ - public function getLocaleMetadataFieldNames(): array - { - return ['submissionLanguage']; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionLanguage', '\SubmissionLanguage'); -} diff --git a/classes/submission/SubmissionLanguageEntryDAO.php b/classes/submission/SubmissionLanguageEntryDAO.php deleted file mode 100644 index 44055393c5a..00000000000 --- a/classes/submission/SubmissionLanguageEntryDAO.php +++ /dev/null @@ -1,64 +0,0 @@ - Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - assert($filter == null); // Parent class supports this, but this class does not - $result = $this->retrieveRange( - 'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq', - [(int) $controlledVocabId], - $rangeInfo - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionLanguageEntryDAO', '\SubmissionLanguageEntryDAO'); -} diff --git a/classes/submission/SubmissionSubject.php b/classes/submission/SubmissionSubject.php deleted file mode 100644 index 23ab1950f60..00000000000 --- a/classes/submission/SubmissionSubject.php +++ /dev/null @@ -1,59 +0,0 @@ -getData('submissionSubject'); - } - - /** - * Set the subject text - * - * @param string $subject - * @param string $locale - */ - public function setSubject($subject, $locale) - { - $this->setData('submissionSubject', $subject, $locale); - } - - /** - * @copydoc \PKP\controlledVocab\ControlledVocabEntry::getLocaleMetadataFieldNames() - */ - public function getLocaleMetadataFieldNames(): array - { - return ['submissionSubject']; - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionSubject', '\SubmissionSubject'); -} diff --git a/classes/submission/SubmissionSubjectEntryDAO.php b/classes/submission/SubmissionSubjectEntryDAO.php deleted file mode 100644 index 2ab01ec30b4..00000000000 --- a/classes/submission/SubmissionSubjectEntryDAO.php +++ /dev/null @@ -1,61 +0,0 @@ - Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - assert($filter == null); // Parent class supports this, but this class does not - $result = $this->retrieveRange( - 'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq', - [(int) $controlledVocabId], - $rangeInfo - ); - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\submission\SubmissionSubjectEntryDAO', '\SubmissionSubjectEntryDAO'); -} diff --git a/classes/user/InterestEntry.php b/classes/user/InterestEntry.php deleted file mode 100644 index f87ac7c0b2b..00000000000 --- a/classes/user/InterestEntry.php +++ /dev/null @@ -1,46 +0,0 @@ -getData('interest'); - } - - /** - * Set the interest text - * - * @param string $interest - */ - public function setInterest($interest) - { - $this->setData('interest', $interest); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\user\InterestEntry', '\InterestEntry'); -} diff --git a/classes/user/InterestEntryDAO.php b/classes/user/InterestEntryDAO.php deleted file mode 100644 index 0df34412f50..00000000000 --- a/classes/user/InterestEntryDAO.php +++ /dev/null @@ -1,100 +0,0 @@ - Object containing matching CVE objects - */ - public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null) - { - $params = [(int) $controlledVocabId]; - if ($filter) { - $params[] = 'interest'; - $params[] = $filter . '%'; - } - - $result = $this->retrieveRange( - 'SELECT cve.* - FROM controlled_vocab_entries cve - JOIN user_interests ui ON (cve.controlled_vocab_entry_id = ui.controlled_vocab_entry_id) - ' . ($filter ? 'JOIN controlled_vocab_entry_settings cves ON (cves.controlled_vocab_entry_id = cve.controlled_vocab_entry_id)' : '') . ' - WHERE cve.controlled_vocab_id = ? - ' . ($filter ? 'AND cves.setting_name=? AND LOWER(cves.setting_value) LIKE LOWER(?)' : '') . ' - GROUP BY cve.controlled_vocab_entry_id - ORDER BY seq', - $params, - $rangeInfo - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } - - /** - * Retrieve controlled vocab entries matching a list of vocab entry IDs - * - * @param array $entryIds - * - * @return DAOResultFactory - */ - public function getByIds($entryIds) - { - $entryString = join(',', array_map(intval(...), $entryIds)); - - $result = $this->retrieve( - 'SELECT * FROM controlled_vocab_entries WHERE controlled_vocab_entry_id IN (' . $entryString . ')' - ); - - return new DAOResultFactory($result, $this, '_fromRow'); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\user\InterestEntryDAO', '\InterestEntryDAO'); -} diff --git a/classes/user/InterestManager.php b/classes/user/InterestManager.php index 5a918441b1c..d13487af825 100644 --- a/classes/user/InterestManager.php +++ b/classes/user/InterestManager.php @@ -15,7 +15,7 @@ namespace PKP\user; use APP\facades\Repo; -use PKP\db\DAORegistry; +use PKP\controlledVocab\ControlledVocabEntry; use PKP\user\User; use PKP\user\interest\UserInterest; @@ -26,14 +26,23 @@ class InterestManager */ public function getAllInterests(?string $filter = null): array { - $interests = Repo::userInterest()->getAllInterests($filter); + $controlledVocab = Repo::controlledVocab()->build( + UserInterest::CONTROLLED_VOCAB_INTEREST + ); - $interestReturner = []; - while ($interest = $interests->next()) { - $interestReturner[] = $interest->getInterest(); - } - - return $interestReturner; + return ControlledVocabEntry::query() + ->withControlledVocabId($controlledVocab->id) + ->when( + $filter, + fn($query) => $query->withSetting( + UserInterest::CONTROLLED_VOCAB_INTEREST, + $filter + ) + ) + ->get() + ->sortBy(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->toArray(); } /** @@ -41,23 +50,17 @@ public function getAllInterests(?string $filter = null): array */ public function getInterestsForUser(User $user): array { - static $interestsCache = []; - $interests = []; - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ - $controlledVocab = Repo::controlledVocab()->build(UserInterest::CONTROLLED_VOCAB_INTEREST); - - foreach (Repo::userInterest()->getUserInterestIds($user->getId()) as $interestEntryId) { - /** @var InterestEntry */ - $interestEntry = $interestsCache[$interestEntryId] ??= $interestEntryDao->getById( - $interestEntryId, - $controlledVocab->id - ); - if ($interestEntry) { - $interests[] = $interestEntry->getInterest(); - } - } - - return $interests; + return ControlledVocabEntry::query() + ->whereHas( + "controlledVocab", + fn($query) => $query + ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->withAssoc(0, 0) + ) + ->whereHas("userInterest", fn($query) => $query->withUserId($user->getId())) + ->get() + ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST, 'id') + ->toArray(); } /** diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 5cb7661da0d..eeb7f4c1838 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -15,126 +15,76 @@ namespace PKP\user\interest; use APP\facades\Repo; -use PKP\db\DAORegistry; -use PKP\user\InterestEntry; -use PKP\user\InterestEntryDAO; -use PKP\core\ArrayItemIterator; use Illuminate\Support\Facades\DB; use PKP\user\interest\UserInterest; -use Illuminate\Database\Query\JoinClause; +use PKP\controlledVocab\ControlledVocabEntry; +use Throwable; class Repository { - - /** - * Get a list of controlled vocabulary entry IDs (corresponding to interest keywords) - * attributed to a user - */ - public function getUserInterestIds(int $userId): array - { - $controlledVocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST - ); - - return DB::table('controlled_vocab_entries AS cve') - ->select(['cve.controlled_vocab_entry_id']) - ->join( - 'user_interests AS ui', - fn (JoinClause $join) => $join - ->on('cve.controlled_vocab_entry_id', '=', 'ui.controlled_vocab_entry_id') - ->where('ui.user_id', $userId) - ) - ->where('controlled_vocab_id', $controlledVocab->id) - ->get() - ->pluck('controlled_vocab_entry_id') - ->toArray(); - } - - /** - * Get a list of user IDs attributed to an interest - */ - public function getUserIdsByInterest(string $interest): array - { - return DB::table('user_interests AS ui') - ->select('ui.user_id') - ->join( - 'controlled_vocab_entry_settings AS cves', - fn (JoinClause $join) => $join - ->on('cves.controlled_vocab_entry_id', '=', 'ui.controlled_vocab_entry_id') - ->where('cves.setting_name', UserInterest::CONTROLLED_VOCAB_INTEREST) - ->where(DB::raw('LOWER(cves.setting_value)'), trim(strtolower($interest))) - ) - ->get() - ->pluck('user_id') - ->toArray(); - } - - - /** - * Get all user's interests - */ - public function getAllInterests(?string $filter = null): object - { - $controlledVocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST - ); - - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var InterestEntryDAO $interestEntryDao */ - $iterator = $interestEntryDao->getByControlledVocabId($controlledVocab->id, null, $filter); - - // Sort by name. - $interests = $iterator->toArray(); - usort($interests, function ($s1, $s2) { - return strcmp($s1->getInterest(), $s2->getInterest()); - }); - - // Turn back into an iterator. - return new ArrayItemIterator($interests); - } - /** * Update a user's set of interests */ - public function setUserInterests(array $interests, int $userId): void + public function setUserInterests(array $interests, int $userId): bool { $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST ); - /** @var InterestEntryDAO $interestEntryDao */ - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); - - DB::beginTransaction(); + $currentInterests = ControlledVocabEntry::query() + ->whereHas( + 'controlledVocab', + fn($query) => $query + ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->withAssoc(0, 0) + ) + ->withLocale('') + ->withSetting(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) + ->get(); + + try { + + DB::beginTransaction(); + + // Delete the existing interests association. + UserInterest::query()->withUserId($userId)->delete(); + + $newInterestIds = collect( + array_diff( + $interests, + $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->toArray() + ) + ) + ->map(fn (string $interest): string => trim($interest)) + ->unique() + ->map( + fn (string $interest) => ControlledVocabEntry::create([ + 'controlledVocabId' => $controlledVocab->id, + UserInterest::CONTROLLED_VOCAB_INTEREST => [ + '' => $interest + ], + ])->id + ); + + // TODO: Investigate the impact of applied patch from https://github.com/pkp/pkp-lib/issues/10423 + collect($currentInterests->pluck('id')) + ->merge($newInterestIds) + ->each(fn ($interestId) => UserInterest::create([ + 'userId' => $userId, + 'controlledVocabEntryId' => $interestId, + ])); + + // TODO: Should Resequence? + + DB::commit(); - // Delete the existing interests association. - UserInterest::withUserId($userId)->delete(); + return true; - collect($interests) - ->map(fn (string $interest): string => trim($interest)) - ->unique() - ->each(function (string $interest) use ($controlledVocab, $interestEntryDao, $userId): void { - $interestEntry = $interestEntryDao->getBySetting( - $interest, - $controlledVocab->symbolic, - $controlledVocab->assocId, - $controlledVocab->assocType, - $controlledVocab->symbolic - ); + } catch (Throwable $exception) { - if (!$interestEntry) { - $interestEntry = $interestEntryDao->newDataObject(); /** @var InterestEntry $interestEntry */ - $interestEntry->setInterest($interest); - $interestEntry->setControlledVocabId($controlledVocab->id); - $interestEntry->setId($interestEntryDao->insertObject($interestEntry)); - } + DB::rollBack(); + } - // TODO: Investigate the impact of applied patch from https://github.com/pkp/pkp-lib/issues/10423 - UserInterest::create([ - 'userId' => $userId, - 'controlledVocabEntryId' => $interestEntry->getId(), - ]); - }); - - DB::commit(); + return false; } } diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index b7d8595b77c..68b1ff24a67 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -15,11 +15,13 @@ namespace PKP\user\interest; use Eloquence\Behaviours\HasCamelCasing; +use Illuminate\Database\Eloquent\Relations\HasMany; +use PKP\controlledVocab\ControlledVocabEntry; +use APP\facades\Repo; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Builder; - class UserInterest extends Model { use HasCamelCasing; @@ -27,39 +29,29 @@ class UserInterest extends Model public const CONTROLLED_VOCAB_INTEREST = 'interest'; /** - * The table associated with the model. - * - * @var string + * @copydoc \Illuminate\Database\Eloquent\Model::$table */ protected $table = 'user_interests'; /** - * The primary key for the model. - * - * @var string + * @copydoc \Illuminate\Database\Eloquent\Model::$primaryKey */ protected $primaryKey = 'user_interest_id'; /** - * The attributes that aren't mass assignable. - * - * @var array|bool + * @copydoc \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::$guarded */ protected $guarded = [ 'user_interest_id', ]; /** - * Indicates if the model should be timestamped. - * - * @var bool + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasTimestamps::$timestamps */ public $timestamps = false; /** - * Get the attributes that should be cast. - * - * @return array + * @copydoc \Illuminate\Database\Eloquent\Concerns\HasAttributes::casts */ protected function casts(): array { @@ -77,9 +69,29 @@ protected function id(): Attribute return Attribute::make( get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, set: fn($value) => [$this->primaryKey => $value], - ); + )->shouldCache(); + } + + /** + * Accessor for user. + * Should replace with relationship once User is converted to an Eloquent Model. + */ + protected function user(): Attribute + { + return Attribute::make( + get: fn () => Repo::user()->get($this->userId, true), + )->shouldCache(); + } + + /** + * Get all the controlled vocab entries for this user interest + */ + public function controlledVocabEntries(): HasMany + { + return $this->hasMany(ControlledVocabEntry::class, 'controlled_vocab_entry_id', 'controlled_vocab_entry_id'); } + // TODO: Investigate if this is necessary anymore /** * Compatibility function for including note IDs in grids. * @@ -91,10 +103,18 @@ public function getId(): int } /** - * Scope a query to only include notes with a specific assoc type and assoc ID. + * Scope a query to only include interests with a specific user id */ public function scopeWithUserId(Builder $query, int $userId): Builder { return $query->where('user_id', $userId); } + + /** + * Scope a query to only include interest with a specific controlled vocab entry id + */ + public function scopeWithControlledVocabEntryId(Builder $query, int $controlledVocabEntryId): Builder + { + return $query->where('controlled_vocab_entry_id', $controlledVocabEntryId); + } } diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index b43bd08050f..4e806b74a0a 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -16,6 +16,7 @@ use APP\facades\Repo; use APP\submission\Submission; use Illuminate\Support\Enumerable; +use PKP\user\interest\UserInterest; use PKP\db\DAORegistry; use PKP\plugins\Hook; use PKP\security\Role; @@ -186,17 +187,13 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat case 'interests': $output[$prop] = []; if ($this->context) { - $interestEntryIds = Repo::userInterest()->getUserInterestIds($user->getId()); - if (!empty($interestEntryIds)) { - $interestEntryDao = DAORegistry::getDAO('InterestEntryDAO'); /** @var \PKP\user\InterestEntryDAO $interestEntryDao */ - $results = $interestEntryDao->getByIds($interestEntryIds); - $output[$prop] = []; - while ($interest = $results->next()) { /** @var \PKP\user\InterestEntry $interest */ - $output[$prop][] = [ - 'id' => (int) $interest->getId(), - 'interest' => $interest->getInterest(), - ]; - } + $interests = collect((new InterestManager())->getInterestsForUser($user)) + ->map(fn($value, $index) => ['id' => $index, 'interest' => $value]) + ->values() + ->toArray(); + + foreach ($interests as $interest) { + $output[$prop][] = $interest; } } break; diff --git a/classes/validation/ValidatorControlledVocab.php b/classes/validation/ValidatorControlledVocab.php index ebd9539c303..c6c30cd62ba 100644 --- a/classes/validation/ValidatorControlledVocab.php +++ b/classes/validation/ValidatorControlledVocab.php @@ -21,14 +21,16 @@ class ValidatorControlledVocab extends Validator { - public array $acceptedValues; + protected array $acceptedValues; /** * Constructor */ public function __construct(string $symbolic, int $assocType, int $assocId) { - $controlledVocab = ControlledVocab::withSymbolic($symbolic) + /** @var ControlledVocab $controlledVocab */ + $controlledVocab = ControlledVocab::query() + ->withSymbolic($symbolic) ->withAssoc($assocType, $assocId) ->first(); diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index 8c7ab043641..98dfda4367c 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -15,14 +15,15 @@ namespace PKP\tests\classes\form\validation; use APP\core\Application; -use PKP\db\DAORegistry; -use PKP\controlledVocab\ControlledVocabEntryDAO; use APP\facades\Repo; +use Illuminate\Support\Facades\DB; use PKP\form\validation\FormValidatorControlledVocab; use PKP\form\Form; use PKP\form\validation\FormValidator; use PKP\tests\PKPTestCase; use PHPUnit\Framework\Attributes\CoversClass; +use PKP\controlledVocab\ControlledVocab; +use PKP\controlledVocab\ControlledVocabEntry; #[CoversClass(FormValidatorControlledVocab::class)] class FormValidatorControlledVocabTest extends PKPTestCase @@ -32,24 +33,32 @@ public function testIsValid() // Test form $form = new Form('some template'); + $assocId = (DB::table("publications") + ->select("publication_id as id") + ->orderBy("publication_id", "desc") + ->first() + ->id ?? 0) + 100; + $testControlledVocab = Repo::controlledVocab()->build( - 'testVocab', + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - 333 + $assocId ); - /** @var ControlledVocabEntryDAO */ - $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); - - $testControlledVocabEntry1 = $controlledVocabEntryDao->newDataObject(); - $testControlledVocabEntry1->setName('testEntry', 'en'); - $testControlledVocabEntry1->setControlledVocabId($testControlledVocab->id); - $controlledVocabEntryId1 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry1); + // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table + $controlledVocabEntryId1 = ControlledVocabEntry::create([ + 'controlledVocabId' => $testControlledVocab->id, + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'en' => 'testEntry', + ], + ])->id; - $testControlledVocabEntry2 = $controlledVocabEntryDao->newDataObject(); - $testControlledVocabEntry2->setName('testEntry', 'en'); - $testControlledVocabEntry2->setControlledVocabId($testControlledVocab->id); - $controlledVocabEntryId2 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry2); + $controlledVocabEntryId2 = ControlledVocabEntry::create([ + 'controlledVocabId' => $testControlledVocab->id, + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'en' => 'testEntry', + ], + ])->id; // Instantiate validator $validator = new FormValidatorControlledVocab( @@ -57,9 +66,9 @@ public function testIsValid() 'testData', FormValidator::FORM_VALIDATOR_REQUIRED_VALUE, 'some.message.key', - 'testVocab', + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - 333 + $assocId ); $form->setData('testData', $controlledVocabEntryId1); @@ -71,9 +80,7 @@ public function testIsValid() $form->setData('testData', 3); self::assertFalse($validator->isValid()); - // Delete the test entried - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId1); - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId2); - $testControlledVocab->delete(); + // Delete the test entries + ControlledVocab::find($testControlledVocab->id)->delete(); } } diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index a8c79d7e17d..f9576e7b8d8 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -20,13 +20,13 @@ use APP\facades\Repo; use InvalidArgumentException; -use PKP\controlledVocab\ControlledVocabEntryDAO; -use PKP\db\DAORegistry; +use PKP\controlledVocab\ControlledVocabEntry; use PKP\metadata\MetadataDescription; use PKP\metadata\MetadataProperty; use PKP\tests\PKPTestCase; use stdClass; use PHPUnit\Framework\Attributes\CoversClass; +use PKP\user\interest\UserInterest; #[CoversClass(MetadataProperty::class)] class MetadataPropertyTest extends PKPTestCase @@ -146,34 +146,35 @@ public function testValidateUri() public function testValidateControlledVocabulary() { - // Build a test vocabulary. (Assoc type and id are 0 to - // simulate a site-wide vocabulary). - $testControlledVocab = Repo::controlledVocab()->build('test-controlled-vocab', 0, 0); - - // Make a vocabulary entry - /** @var ControlledVocabEntryDAO */ - $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); - $testControlledVocabEntry = $controlledVocabEntryDao->newDataObject(); - $testControlledVocabEntry->setName('testEntry', 'en'); - $testControlledVocabEntry->setControlledVocabId($testControlledVocab->id); - $controlledVocabEntryId = $controlledVocabEntryDao->insertObject($testControlledVocabEntry); + // Build a test vocabulary. (Assoc type and id are 0 to simulate a site-wide vocabulary). + $vocab = Repo::controlledVocab()->build( + UserInterest::CONTROLLED_VOCAB_INTEREST, 0, 0 + ); + + // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table + $controlledVocabEntry = ControlledVocabEntry::create([ + 'controlledVocabId' => $vocab->id, + UserInterest::CONTROLLED_VOCAB_INTEREST => [ + 'en' => 'testEntry', + ], + ]); $metadataProperty = new MetadataProperty( 'testElement', [], - [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab'] + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => UserInterest::CONTROLLED_VOCAB_INTEREST] ); // This validator checks numeric values self::assertEquals( - [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => 'test-controlled-vocab'], - $metadataProperty->isValid($controlledVocabEntryId) + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => UserInterest::CONTROLLED_VOCAB_INTEREST], + $metadataProperty->isValid($controlledVocabEntry->id) ); - self::assertFalse($metadataProperty->isValid($controlledVocabEntryId + 1)); + self::assertFalse($metadataProperty->isValid($controlledVocabEntry->id + 1)); - // Delete the test vocabulary - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId); - $testControlledVocab->delete(); + // Delete the test vocabulary entry + $controlledVocabEntry->delete(); + } public function testValidateDate() diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index 3d586b99b8d..f81678e6787 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -16,8 +16,10 @@ use APP\core\Application; use APP\facades\Repo; +use Illuminate\Support\Facades\DB; +use PKP\controlledVocab\ControlledVocabEntry; +use PKP\controlledVocab\ControlledVocab; use PHPUnit\Framework\Attributes\CoversClass; -use PKP\db\DAORegistry; use PKP\tests\PKPTestCase; use PKP\validation\ValidatorControlledVocab; @@ -26,33 +28,43 @@ class ValidatorControlledVocabTest extends PKPTestCase { public function testValidatorControlledVocab() { + $assocId = (DB::table("publications") + ->select("publication_id as id") + ->orderBy("publication_id", "desc") + ->first() + ->id ?? 0) + 100; + $testControlledVocab = Repo::controlledVocab()->build( - 'testVocab', + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - 333 + $assocId ); - /** @var ControlledVocabEntryDAO */ - $controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); + // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table + $controlledVocabEntryId1 = ControlledVocabEntry::create([ + 'controlledVocabId' => $testControlledVocab->id, + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'en' => 'testEntry', + ], + ])->id; - $testControlledVocabEntry1 = $controlledVocabEntryDao->newDataObject(); - $testControlledVocabEntry1->setName('testEntry', 'en'); - $testControlledVocabEntry1->setControlledVocabId($testControlledVocab->id); - $controlledVocabEntryId1 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry1); + $controlledVocabEntryId2 = ControlledVocabEntry::create([ + 'controlledVocabId' => $testControlledVocab->id, + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'en' => 'testEntry', + ], + ])->id; - $testControlledVocabEntry2 = $controlledVocabEntryDao->newDataObject(); - $testControlledVocabEntry2->setName('testEntry', 'en'); - $testControlledVocabEntry2->setControlledVocabId($testControlledVocab->id); - $controlledVocabEntryId2 = $controlledVocabEntryDao->insertObject($testControlledVocabEntry2); - - $validator = new ValidatorControlledVocab('testVocab', Application::ASSOC_TYPE_CITATION, 333); + $validator = new ValidatorControlledVocab( + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, + Application::ASSOC_TYPE_CITATION, + $assocId + ); self::assertTrue($validator->isValid($controlledVocabEntryId1)); self::assertTrue($validator->isValid($controlledVocabEntryId2)); self::assertFalse($validator->isValid(3)); // Delete the test entried - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId1); - $controlledVocabEntryDao->deleteObjectById($controlledVocabEntryId2); - $testControlledVocab->delete(); + ControlledVocab::find($testControlledVocab->id)->delete(); } } From a97e1bf886612682d867db918b647a5f0733cbd2 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 7 Oct 2024 19:03:49 +0600 Subject: [PATCH 11/30] pkp/pkp-lib#10292 removed setting_type column --- .../install/ControlledVocabMigration.php | 1 - ..._RemoveControlledVocabEntrySettingType.php | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 classes/migration/upgrade/v3_5_0/I10292_RemoveControlledVocabEntrySettingType.php diff --git a/classes/migration/install/ControlledVocabMigration.php b/classes/migration/install/ControlledVocabMigration.php index e94aec660b4..431184bf15a 100644 --- a/classes/migration/install/ControlledVocabMigration.php +++ b/classes/migration/install/ControlledVocabMigration.php @@ -58,7 +58,6 @@ public function up(): void $table->string('locale', 28)->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'); }); diff --git a/classes/migration/upgrade/v3_5_0/I10292_RemoveControlledVocabEntrySettingType.php b/classes/migration/upgrade/v3_5_0/I10292_RemoveControlledVocabEntrySettingType.php new file mode 100644 index 00000000000..a29e54c9212 --- /dev/null +++ b/classes/migration/upgrade/v3_5_0/I10292_RemoveControlledVocabEntrySettingType.php @@ -0,0 +1,42 @@ +dropColumn('setting_type'); + }); + } + + /** + * Reverse the migration. + */ + public function down(): void + { + Schema::table('controlled_vocab_entry_settings', function (Blueprint $table) { + $table->string('setting_type', 6); + }); + } +} From bbeedaecec708e7fa3b8d2a028345223e5031781 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 8 Oct 2024 09:57:55 +0600 Subject: [PATCH 12/30] pkp/pkp-lib#10292 fixed vocab setting issue --- classes/controlledVocab/Repository.php | 60 ++++++++++---------------- classes/user/interest/Repository.php | 10 ++--- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index bbaf23c1c7f..5edc4e2cae1 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -52,9 +52,9 @@ public function getBySymbolic( ControlledVocabEntry::query() ->whereHas( "controlledVocab", - fn($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) + fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) ) - ->withLocale($locales) + ->when(!empty($locales), fn ($query) => $query->withLocale($locales)) ->get() ->each(function ($entry) use (&$result, $symbolic) { foreach ($entry->{$symbolic} as $locale => $value) { @@ -65,20 +65,6 @@ public function getBySymbolic( return $result; } - /** - * Get an array of all of the vocabs for given symbolic - */ - public function getAllUniqueBySymbolic(string $symbolic): array - { - return DB::table('controlled_vocab_entry_settings') - ->select('setting_value') - ->where('setting_name', $symbolic) - ->distinct() - ->get() - ->pluck('setting_value') - ->toArray(); - } - /** * Add an array of vocabs */ @@ -88,7 +74,7 @@ public function insertBySymbolic( int $assocType, int $assocId, bool $deleteFirst = true, - ): bool + ): void { $controlledVocab = $this->build($symbolic, $assocType, $assocId); $controlledVocab->load('controlledVocabEntries'); @@ -98,39 +84,37 @@ public function insertBySymbolic( DB::beginTransaction(); if ($deleteFirst) { - ControlledVocabEntry::whereIn( - 'id', - $controlledVocab->controlledVocabEntries->pluck('id')->toArray() - )->delete(); + ControlledVocabEntry::query() + ->whereIn( + (new ControlledVocabEntry)->getKeyName(), + $controlledVocab->controlledVocabEntries->pluck('id')->toArray() + ) + ->delete(); } collect($vocabs) ->each( - fn (array|string $entries, string $locale) => collect(Arr::wrap($entries)) + fn (array|string $entries, string $locale) => collect(array_values(Arr::wrap($entries))) ->each( - fn (string $vocab, $seq = 1) => + fn (string $vocab, int $index) => ControlledVocabEntry::create([ 'controlledVocabId' => $controlledVocab->id, - 'seq' => $seq, + 'seq' => $index + 1, "{$symbolic}" => [ $locale => $vocab ], ]) ) ); - - // TODO: Should Resequence? - - DB::commit(); - return true; + DB::commit(); } catch (Throwable $exception) { - + DB::rollBack(); - } - return false; + throw $exception; + } } /** @@ -138,12 +122,14 @@ public function insertBySymbolic( */ public function resequence(int $controlledVocabId): void { + $seq = 1; + ControlledVocabEntry::query() ->withControlledVocabId($controlledVocabId) - ->each( - fn ($controlledVocabEntry, $seq = 1) => $controlledVocabEntry->update([ - 'seq' => $seq, - ]) - ); + ->each(function ($controlledVocabEntry) use (&$seq) { + $controlledVocabEntry->update([ + 'seq' => $seq++, + ]); + }); } } diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index eeb7f4c1838..832c7916851 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -25,7 +25,7 @@ class Repository /** * Update a user's set of interests */ - public function setUserInterests(array $interests, int $userId): bool + public function setUserInterests(array $interests, int $userId): void { $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST @@ -74,17 +74,15 @@ public function setUserInterests(array $interests, int $userId): bool 'controlledVocabEntryId' => $interestId, ])); - // TODO: Should Resequence? + Repo::controlledVocab()->resequence($controlledVocab->id); DB::commit(); - return true; - } catch (Throwable $exception) { DB::rollBack(); - } - return false; + throw $exception; + } } } From 5b4bbbd92824c5be48c565ab94c80e62d95db9ee Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 8 Oct 2024 15:51:02 +0600 Subject: [PATCH 13/30] pkp/pkp-lib#10292 refactored interest manager to repo and removed it --- classes/user/InterestManager.php | 91 ------------------- classes/user/User.php | 4 +- classes/user/form/RegistrationForm.php | 4 +- classes/user/form/RolesForm.php | 8 +- classes/user/interest/Repository.php | 65 ++++++++++++- classes/user/maps/Schema.php | 3 +- .../settings/user/form/UserDetailsForm.php | 7 +- .../reviewer/form/CreateReviewerForm.php | 4 +- pages/user/PKPUserHandler.php | 4 +- .../users/filter/PKPUserUserXmlFilter.php | 4 +- .../users/filter/UserXmlPKPUserFilter.php | 4 +- 11 files changed, 74 insertions(+), 124 deletions(-) delete mode 100644 classes/user/InterestManager.php diff --git a/classes/user/InterestManager.php b/classes/user/InterestManager.php deleted file mode 100644 index d13487af825..00000000000 --- a/classes/user/InterestManager.php +++ /dev/null @@ -1,91 +0,0 @@ -build( - UserInterest::CONTROLLED_VOCAB_INTEREST - ); - - return ControlledVocabEntry::query() - ->withControlledVocabId($controlledVocab->id) - ->when( - $filter, - fn($query) => $query->withSetting( - UserInterest::CONTROLLED_VOCAB_INTEREST, - $filter - ) - ) - ->get() - ->sortBy(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->toArray(); - } - - /** - * Get user reviewing interests. (Cached in memory for batch fetches.) - */ - public function getInterestsForUser(User $user): array - { - return ControlledVocabEntry::query() - ->whereHas( - "controlledVocab", - fn($query) => $query - ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->withAssoc(0, 0) - ) - ->whereHas("userInterest", fn($query) => $query->withUserId($user->getId())) - ->get() - ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST, 'id') - ->toArray(); - } - - /** - * Returns a comma separated string of a user's interests - */ - public function getInterestsString(User $user): string - { - $interests = $this->getInterestsForUser($user); - - return implode(', ', $interests); - } - - /** - * Set a user's interests - */ - public function setInterestsForUser(User $user, string|array|null $interests = null): void - { - $interests = is_array($interests) - ? $interests - : (empty($interests) ? [] : explode(',', $interests)); - - Repo::userInterest()->setUserInterests($interests, $user->getId()); - } -} - -if (!PKP_STRICT_MODE) { - class_alias('\PKP\user\InterestManager', '\InterestManager'); -} diff --git a/classes/user/User.php b/classes/user/User.php index e0c95930e0b..122696beb1f 100644 --- a/classes/user/User.php +++ b/classes/user/User.php @@ -20,6 +20,7 @@ namespace PKP\user; +use APP\facades\Repo; use Illuminate\Contracts\Auth\Authenticatable; use PKP\db\DAORegistry; use PKP\identity\Identity; @@ -192,8 +193,7 @@ public function setBillingAddress($billingAddress) */ public function getInterestString() { - $interestManager = new InterestManager(); - return $interestManager->getInterestsString($this); + return Repo::userInterest()->getInterestsString($this); } /** diff --git a/classes/user/form/RegistrationForm.php b/classes/user/form/RegistrationForm.php index 3ec718003b3..215fc44c253 100644 --- a/classes/user/form/RegistrationForm.php +++ b/classes/user/form/RegistrationForm.php @@ -32,7 +32,6 @@ use PKP\security\Role; use PKP\security\Validation; use PKP\site\Site; -use PKP\user\InterestManager; use PKP\user\User; use PKP\userGroup\UserGroup; @@ -327,8 +326,7 @@ public function execute(...$functionArgs) } // Insert the user interests - $interestManager = new InterestManager(); - $interestManager->setInterestsForUser($user, $this->getData('interests')); + Repo::userInterest()->setInterestsForUser($user, $this->getData('interests')); return $userId; } diff --git a/classes/user/form/RolesForm.php b/classes/user/form/RolesForm.php index 7f5752b4165..e975491e209 100644 --- a/classes/user/form/RolesForm.php +++ b/classes/user/form/RolesForm.php @@ -18,7 +18,6 @@ use APP\core\Application; use APP\template\TemplateManager; -use PKP\user\InterestManager; use PKP\user\User; use PKP\userGroup\UserGroup; @@ -58,12 +57,10 @@ public function fetch($request, $template = null, $display = false) */ public function initData() { - $interestManager = new InterestManager(); - $user = $this->getUser(); $this->_data = [ - 'interests' => $interestManager->getInterestsForUser($user), + 'interests' => Repo::userInterest()->getInterestsForUser($user), ]; } @@ -95,8 +92,7 @@ public function execute(...$functionArgs) $userFormHelper->saveRoleContent($this, $user); // Insert the user interests - $interestManager = new InterestManager(); - $interestManager->setInterestsForUser($user, $this->getData('interests')); + Repo::userInterest()->setInterestsForUser($user, $this->getData('interests')); parent::execute(...$functionArgs); } diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 832c7916851..645ec6879bc 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -16,6 +16,7 @@ use APP\facades\Repo; use Illuminate\Support\Facades\DB; +use PKP\user\User; use PKP\user\interest\UserInterest; use PKP\controlledVocab\ControlledVocabEntry; use Throwable; @@ -23,14 +24,70 @@ class Repository { /** - * Update a user's set of interests + * Get all interests for all users in the system */ - public function setUserInterests(array $interests, int $userId): void + public function getAllInterests(?string $filter = null): array { $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST ); + return ControlledVocabEntry::query() + ->withControlledVocabId($controlledVocab->id) + ->when( + $filter, + fn($query) => $query->withSetting( + UserInterest::CONTROLLED_VOCAB_INTEREST, + $filter + ) + ) + ->get() + ->sortBy(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->toArray(); + } + + /** + * Get user reviewing interests. (Cached in memory for batch fetches.) + */ + public function getInterestsForUser(User $user): array + { + return ControlledVocabEntry::query() + ->whereHas( + "controlledVocab", + fn($query) => $query + ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->withAssoc(0, 0) + ) + ->whereHas("userInterest", fn($query) => $query->withUserId($user->getId())) + ->get() + ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST, 'id') + ->toArray(); + } + + /** + * Returns a comma separated string of a user's interests + */ + public function getInterestsString(User $user): string + { + $interests = $this->getInterestsForUser($user); + + return implode(', ', $interests); + } + + /** + * Set a user's interests + */ + public function setInterestsForUser(User $user, string|array|null $interests = null): void + { + $interests = is_array($interests) + ? $interests + : (empty($interests) ? [] : explode(',', $interests)); + + $controlledVocab = Repo::controlledVocab()->build( + UserInterest::CONTROLLED_VOCAB_INTEREST + ); + $currentInterests = ControlledVocabEntry::query() ->whereHas( 'controlledVocab', @@ -47,7 +104,7 @@ public function setUserInterests(array $interests, int $userId): void DB::beginTransaction(); // Delete the existing interests association. - UserInterest::query()->withUserId($userId)->delete(); + UserInterest::query()->withUserId($user->getId())->delete(); $newInterestIds = collect( array_diff( @@ -70,7 +127,7 @@ public function setUserInterests(array $interests, int $userId): void collect($currentInterests->pluck('id')) ->merge($newInterestIds) ->each(fn ($interestId) => UserInterest::create([ - 'userId' => $userId, + 'userId' => $user->getId(), 'controlledVocabEntryId' => $interestId, ])); diff --git a/classes/user/maps/Schema.php b/classes/user/maps/Schema.php index 4e806b74a0a..93b86f3847a 100644 --- a/classes/user/maps/Schema.php +++ b/classes/user/maps/Schema.php @@ -16,7 +16,6 @@ use APP\facades\Repo; use APP\submission\Submission; use Illuminate\Support\Enumerable; -use PKP\user\interest\UserInterest; use PKP\db\DAORegistry; use PKP\plugins\Hook; use PKP\security\Role; @@ -187,7 +186,7 @@ protected function mapByProperties(array $props, User $user, array $auxiliaryDat case 'interests': $output[$prop] = []; if ($this->context) { - $interests = collect((new InterestManager())->getInterestsForUser($user)) + $interests = collect(Repo::userInterest()->getInterestsForUser($user)) ->map(fn($value, $index) => ['id' => $index, 'interest' => $value]) ->values() ->toArray(); diff --git a/controllers/grid/settings/user/form/UserDetailsForm.php b/controllers/grid/settings/user/form/UserDetailsForm.php index f5e4a0b5223..a36ddd242de 100644 --- a/controllers/grid/settings/user/form/UserDetailsForm.php +++ b/controllers/grid/settings/user/form/UserDetailsForm.php @@ -31,7 +31,6 @@ use PKP\mail\mailables\UserCreated; use PKP\notification\Notification; use PKP\security\Validation; -use PKP\user\InterestManager; use PKP\user\User; use Symfony\Component\Mailer\Exception\TransportException; @@ -154,7 +153,6 @@ public function initData() if (isset($this->user)) { $user = $this->user; $templateMgr->assign('user', $user); - $interestManager = new InterestManager(); $data = [ 'username' => $user->getUsername(), @@ -169,7 +167,7 @@ public function initData() 'mailingAddress' => $user->getMailingAddress(), 'country' => $user->getCountry(), 'biography' => $user->getBiography(null), // Localized - 'interests' => $interestManager->getInterestsForUser($user), + 'interests' => Repo::userInterest()->getInterestsForUser($user), 'locales' => $user->getLocales(), ]; $data['canCurrentUserGossip'] = Repo::user()->canCurrentUserGossip($user->getId()); @@ -392,8 +390,7 @@ public function execute(...$functionParams) } } - $interestManager = new InterestManager(); - $interestManager->setInterestsForUser($this->user, $this->getData('interests')); + Repo::userInterest()->setInterestsForUser($this->user, $this->getData('interests')); return $this->user; } diff --git a/controllers/grid/users/reviewer/form/CreateReviewerForm.php b/controllers/grid/users/reviewer/form/CreateReviewerForm.php index e909b54ff0b..e5f280b33bf 100644 --- a/controllers/grid/users/reviewer/form/CreateReviewerForm.php +++ b/controllers/grid/users/reviewer/form/CreateReviewerForm.php @@ -27,7 +27,6 @@ use PKP\notification\Notification; use PKP\security\Validation; use PKP\submission\reviewRound\ReviewRound; -use PKP\user\InterestManager; use Symfony\Component\Mailer\Exception\TransportException; class CreateReviewerForm extends ReviewerForm @@ -143,8 +142,7 @@ public function execute(...$functionArgs) $this->setData('reviewerId', $reviewerId); // Insert the user interests - $interestManager = new InterestManager(); - $interestManager->setInterestsForUser($user, $this->getData('interests')); + Repo::userInterest()->setInterestsForUser($user, $this->getData('interests')); // Assign the selected user group ID to the user $userGroupId = (int) $this->getData('userGroupId'); diff --git a/pages/user/PKPUserHandler.php b/pages/user/PKPUserHandler.php index 20f7ec5e6f0..c7a72f1aa0f 100644 --- a/pages/user/PKPUserHandler.php +++ b/pages/user/PKPUserHandler.php @@ -17,12 +17,12 @@ namespace PKP\pages\user; use APP\core\Request; +use APP\facades\Repo; use APP\handler\Handler; use APP\template\TemplateManager; use PKP\core\JSONMessage; use PKP\core\PKPRequest; use PKP\security\Validation; -use PKP\user\InterestManager; class PKPUserHandler extends Handler { @@ -46,7 +46,7 @@ public function getInterests($args, $request) { return new JSONMessage( true, - (new InterestManager())->getAllInterests($request->getUserVar('term')) + Repo::userInterest()->getAllInterests($request->getUserVar('term')) ); } diff --git a/plugins/importexport/users/filter/PKPUserUserXmlFilter.php b/plugins/importexport/users/filter/PKPUserUserXmlFilter.php index 809d5ee8bfd..2445df1c330 100644 --- a/plugins/importexport/users/filter/PKPUserUserXmlFilter.php +++ b/plugins/importexport/users/filter/PKPUserUserXmlFilter.php @@ -21,7 +21,6 @@ use PKP\filter\FilterDAO; use PKP\filter\FilterGroup; use PKP\plugins\importexport\native\filter\NativeExportFilter; -use PKP\user\InterestManager; use PKP\user\User; use PKP\userGroup\UserGroup; @@ -141,8 +140,7 @@ public function createPKPUserNode($doc, $user) } // Add Reviewing Interests, if any. - $interestManager = new InterestManager(); - $interests = $interestManager->getInterestsString($user); + $interests = Repo::userInterest()->getInterestsString($user); $this->createOptionalNode($doc, $userNode, 'review_interests', $interests); return $userNode; diff --git a/plugins/importexport/users/filter/UserXmlPKPUserFilter.php b/plugins/importexport/users/filter/UserXmlPKPUserFilter.php index 37f17241d4f..f7092a47312 100644 --- a/plugins/importexport/users/filter/UserXmlPKPUserFilter.php +++ b/plugins/importexport/users/filter/UserXmlPKPUserFilter.php @@ -26,7 +26,6 @@ use PKP\plugins\importexport\users\PKPUserImportExportDeployment; use PKP\security\Validation; use PKP\site\SiteDAO; -use PKP\user\InterestManager; use PKP\user\User; use PKP\userGroup\UserGroup; @@ -265,8 +264,7 @@ public function parseUser($node) $n = $interestNodeList->item(0); if ($n) { $interests = preg_split('/,\s*/', $n->textContent); - $interestManager = new InterestManager(); - $interestManager->setInterestsForUser($user, $interests); + Repo::userInterest()->setInterestsForUser($user, $interests); } } From 4dd58376eaec05bde67f7f14ed45780466b5ca11 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 8 Oct 2024 17:55:38 +0600 Subject: [PATCH 14/30] pkp/pkp-lib#10292 resolved all but one TODO --- classes/controlledVocab/ControlledVocab.php | 31 +------------------ .../controlledVocab/ControlledVocabEntry.php | 8 ++--- classes/metadata/MetadataProperty.php | 3 +- classes/user/interest/Repository.php | 1 - classes/user/interest/UserInterest.php | 11 ------- .../FormValidatorControlledVocabTest.php | 3 +- .../classes/metadata/MetadataPropertyTest.php | 15 +++++---- .../ValidatorControlledVocabTest.php | 3 +- 8 files changed, 15 insertions(+), 60 deletions(-) diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index 5a11c9bf372..aaadba13d8f 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -104,32 +104,6 @@ public static function hasDefinedVocabSymbolic(string $vocab): bool return in_array($vocab, static::getDefinedVocabSymbolic()); } - // TODO: Investigate if this is necessary anymore - /** - * Get the locale field names for this controlled vocab - */ - public function getLocaleFieldNames(): array - { - if (!$this->symbolic) { - return []; - } - - return static::hasDefinedVocabSymbolic($this->symbolic) - ? [$this->symbolic] - : []; - } - - // TODO: Investigate if this is necessary anymore - /** - * Compatibility function for including note IDs in grids. - * - * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. - */ - public function getId(): int - { - return $this->id; - } - /** * Get all controlled vocab entries for this controlled vocab */ @@ -216,10 +190,7 @@ public function enumerate(?string $settingName = null): array 'e.controlled_vocab_entry_id', DB::raw( 'COALESCE (l.setting_value, p.setting_value, n.setting_value) as setting_value' - ), - DB::raw( - 'COALESCE (l.setting_type, p.setting_type, n.setting_type) as setting_type' - ), + ) ]) ->where('e.controlled_vocab_id', $this->id) ->orderBy('e.seq') diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 8cced60bf01..19f4825b242 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -52,7 +52,7 @@ class ControlledVocabEntry extends Model public $timestamps = false; /** - * @inheritDoc + * @copydoc \PKP\core\traits\ModelWithSettings::getSettingsTable */ public function getSettingsTable(): string { @@ -72,7 +72,7 @@ protected function casts(): array } /** - * @inheritDoc + * @copydoc \PKP\core\traits\ModelWithSettings::getSchemaName */ public static function getSchemaName(): ?string { @@ -80,7 +80,7 @@ public static function getSchemaName(): ?string } /** - * @inheritDoc + * @copydoc \PKP\core\traits\ModelWithSettings::getMultilingualProps */ public function getMultilingualProps(): array { @@ -92,7 +92,7 @@ public function getMultilingualProps(): array } /** - * @inheritDoc + * @copydoc \PKP\core\traits\ModelWithSettings::getSettings */ public function getSettings(): array { diff --git a/classes/metadata/MetadataProperty.php b/classes/metadata/MetadataProperty.php index 873bda81603..ba15cd9007c 100644 --- a/classes/metadata/MetadataProperty.php +++ b/classes/metadata/MetadataProperty.php @@ -429,8 +429,7 @@ public function isValid($value, $locale = null) fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) ) ->withLocale($locale) - // TODO: Investigate if this need to be 'name' or $symbolic for settingName - ->withSetting('name', $value) + ->withSetting($symbolic, $value) ->first(); if (!is_null($entry)) { diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 645ec6879bc..59ce5efb12c 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -123,7 +123,6 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ])->id ); - // TODO: Investigate the impact of applied patch from https://github.com/pkp/pkp-lib/issues/10423 collect($currentInterests->pluck('id')) ->merge($newInterestIds) ->each(fn ($interestId) => UserInterest::create([ diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index 68b1ff24a67..97961b45ce0 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -91,17 +91,6 @@ public function controlledVocabEntries(): HasMany return $this->hasMany(ControlledVocabEntry::class, 'controlled_vocab_entry_id', 'controlled_vocab_entry_id'); } - // TODO: Investigate if this is necessary anymore - /** - * Compatibility function for including note IDs in grids. - * - * @deprecated 3.5.0 Use $model->id instead. Can be removed once the DataObject pattern is removed. - */ - public function getId(): int - { - return $this->id; - } - /** * Scope a query to only include interests with a specific user id */ diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index 98dfda4367c..c837efe9bb9 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -45,7 +45,6 @@ public function testIsValid() $assocId ); - // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table $controlledVocabEntryId1 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ @@ -80,7 +79,7 @@ public function testIsValid() $form->setData('testData', 3); self::assertFalse($validator->isValid()); - // Delete the test entries + // Delete the test vocab along with entries ControlledVocab::find($testControlledVocab->id)->delete(); } } diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index f9576e7b8d8..7e71fd19f8a 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -20,13 +20,13 @@ use APP\facades\Repo; use InvalidArgumentException; +use PKP\controlledVocab\ControlledVocab; use PKP\controlledVocab\ControlledVocabEntry; use PKP\metadata\MetadataDescription; use PKP\metadata\MetadataProperty; use PKP\tests\PKPTestCase; use stdClass; use PHPUnit\Framework\Attributes\CoversClass; -use PKP\user\interest\UserInterest; #[CoversClass(MetadataProperty::class)] class MetadataPropertyTest extends PKPTestCase @@ -148,13 +148,12 @@ public function testValidateControlledVocabulary() { // Build a test vocabulary. (Assoc type and id are 0 to simulate a site-wide vocabulary). $vocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST, 0, 0 + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 0, 0 ); - // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table $controlledVocabEntry = ControlledVocabEntry::create([ 'controlledVocabId' => $vocab->id, - UserInterest::CONTROLLED_VOCAB_INTEREST => [ + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ 'en' => 'testEntry', ], ]); @@ -162,18 +161,18 @@ public function testValidateControlledVocabulary() $metadataProperty = new MetadataProperty( 'testElement', [], - [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => UserInterest::CONTROLLED_VOCAB_INTEREST] + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD] ); // This validator checks numeric values self::assertEquals( - [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => UserInterest::CONTROLLED_VOCAB_INTEREST], + [MetadataProperty::METADATA_PROPERTY_TYPE_VOCABULARY => ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD], $metadataProperty->isValid($controlledVocabEntry->id) ); self::assertFalse($metadataProperty->isValid($controlledVocabEntry->id + 1)); - // Delete the test vocabulary entry - $controlledVocabEntry->delete(); + // Delete the test vocab along with entry + $vocab->delete(); } diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index f81678e6787..3b760a8ccd7 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -40,7 +40,6 @@ public function testValidatorControlledVocab() $assocId ); - // TODO : Investigate if possible to insert dummy symbolic in `controlled_vocab_entry_settings` table $controlledVocabEntryId1 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ @@ -64,7 +63,7 @@ public function testValidatorControlledVocab() self::assertTrue($validator->isValid($controlledVocabEntryId2)); self::assertFalse($validator->isValid(3)); - // Delete the test entried + // Delete the test vocab along with entries ControlledVocab::find($testControlledVocab->id)->delete(); } } From 6724d7e0dabdc62121599ecad7b7ad25ade747ab Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Sat, 19 Oct 2024 00:30:18 +0600 Subject: [PATCH 15/30] pkp/pkp-lib#10292 fixed issue the settings builder to retrieve specific columns only --- classes/core/SettingsBuilder.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/classes/core/SettingsBuilder.php b/classes/core/SettingsBuilder.php index c698dc3e7b3..131a0135a7d 100644 --- a/classes/core/SettingsBuilder.php +++ b/classes/core/SettingsBuilder.php @@ -39,6 +39,7 @@ class SettingsBuilder extends Builder public function getModels($columns = ['*']) { $rows = $this->getModelWithSettings($columns); + $returner = $this->model->hydrate( $rows->all() )->all(); @@ -308,8 +309,19 @@ protected function filterRow(stdClass $row, string|array $columns = ['*']): stdC } $columns = Arr::wrap($columns); - foreach ($row as $property) { - if (!in_array($property, $columns)) { + + // TODO : Investigate how to handle the camel to snake case issue. related to pkp/pkp-lib#10485 + $settingColumns = $this->model->getSettings(); + $columns = collect($columns) + ->map( + fn (string $column): string => in_array($column, $settingColumns) + ? $column + : Str::snake($column) + ) + ->toArray(); + + foreach ($row as $property => $value) { + if (!in_array($property, $columns) && isset($row->{$property})) { unset($row->{$property}); } } From 29e0695c7d4bf2d83cde91495689bf25318f0ec0 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 21 Oct 2024 17:36:04 +0600 Subject: [PATCH 16/30] pkp/pkp-lib#10292 fixed issue when guarded attribute defined --- .../controlledVocab/ControlledVocabEntry.php | 16 +------ classes/core/traits/ModelWithSettings.php | 46 +++++++++++++++++-- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 19f4825b242..707de4232d7 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -38,12 +38,11 @@ class ControlledVocabEntry extends Model */ protected $primaryKey = 'controlled_vocab_entry_id'; - // TODO: Investigate why defining any guarded props causing no data to store in settings table /** * @copydoc \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::$guarded */ protected $guarded = [ - // 'id', + 'controlledVocabEntryId', ]; /** @@ -66,7 +65,7 @@ protected function casts(): array { return [ 'controlled_vocab_entry_id' => 'string', - 'controlled_vocab_id' => 'int', + 'controlled_vocab_id' => 'integer', 'seq' => 'float', ]; } @@ -103,17 +102,6 @@ public function getSettings(): array ); } - /** - * Accessor and Mutator for primary key => id - */ - protected function id(): Attribute - { - return Attribute::make( - get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, - set: fn($value) => [$this->primaryKey => $value], - )->shouldCache(); - } - /** * The controlled vocab associated with this controlled vocab entry */ diff --git a/classes/core/traits/ModelWithSettings.php b/classes/core/traits/ModelWithSettings.php index a63c1096447..295d61a131c 100644 --- a/classes/core/traits/ModelWithSettings.php +++ b/classes/core/traits/ModelWithSettings.php @@ -17,8 +17,9 @@ namespace PKP\core\traits; -use Eloquence\Behaviours\HasCamelCasing; use Exception; +use Eloquence\Behaviours\HasCamelCasing; +use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Casts\Attribute; use PKP\core\maps\Schema; use PKP\core\SettingsBuilder; @@ -30,6 +31,11 @@ trait ModelWithSettings { use HasCamelCasing; + /** + * @see \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::$guardableColumns + */ + protected static $guardableColumns = []; + // The list of attributes associated with the model settings protected array $settings = []; @@ -46,7 +52,7 @@ abstract public function getTable(); /** * Get settings table name */ - abstract public function getSettingsTable(); + abstract public function getSettingsTable(): string; /** * The name of the schema for the Model if exists, null otherwise @@ -54,7 +60,7 @@ abstract public function getSettingsTable(); abstract public static function getSchemaName(): ?string; /** - * See Illuminate\Database\Eloquent\Concerns\HasAttributes::mergeCasts() + * @see Illuminate\Database\Eloquent\Concerns\HasAttributes::mergeCasts() * * @param array $casts * @@ -62,6 +68,9 @@ abstract public static function getSchemaName(): ?string; */ abstract protected function ensureCastsAreStringValues($casts); + /** + * @see \Illuminate\Database\Eloquent\Model::__construct() + */ public function __construct(array $attributes = []) { parent::__construct($attributes); @@ -188,4 +197,35 @@ protected function id(): Attribute set: fn ($value) => [$this->primaryKey => $value], ); } + + /** + * @see \Illuminate\Database\Eloquent\Concerns\GuardsAttributes::isGuardableColumn() + */ + protected function isGuardableColumn($key) + { + // Need the snake like to key to check for main table to compare with column listing + $key = Str::snake($key); + + if (! isset(static::$guardableColumns[get_class($this)])) { + $columns = $this->getConnection() + ->getSchemaBuilder() + ->getColumnListing($this->getTable()); + + if (empty($columns)) { + return true; + } + static::$guardableColumns[get_class($this)] = $columns; + } + + + $settingsWithMultilingual = array_merge($this->getSettings(), $this->getMultilingualProps()); + $camelKey = Str::camel($key); + + // Check if this column included in setting and multilingula props and not set to guarded + if (in_array($camelKey, $settingsWithMultilingual) && !in_array($camelKey, $this->getGuarded())) { + return true; + } + + return in_array($key, (array)static::$guardableColumns[get_class($this)]); + } } From acd9d45e0af29c0f11022c6a76ec79f13d08c513 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 22 Oct 2024 17:48:34 +0600 Subject: [PATCH 17/30] pkp/pkp-lib#10292 updated implementation of updating with multilingual data --- classes/core/SettingsBuilder.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/classes/core/SettingsBuilder.php b/classes/core/SettingsBuilder.php index 131a0135a7d..926878983e9 100644 --- a/classes/core/SettingsBuilder.php +++ b/classes/core/SettingsBuilder.php @@ -285,7 +285,9 @@ protected function getModelWithSettings(array|string $columns = ['*']): Collecti // Retract the row and fill it with data from a settings table $exactRow = $rows->pull($settingModelId); - if ($setting->locale) { + + // Even for empty('') locale, the multilingual props need to be an array + if (isset($setting->locale)) { $exactRow->{$setting->setting_name}[$setting->locale] = $setting->setting_value; } else { $exactRow->{$setting->setting_name} = $setting->setting_value; From 190cfdcfd28822eb02c65cb27862af88e929d967 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 22 Oct 2024 18:51:28 +0600 Subject: [PATCH 18/30] pkp/pkp-lib#10292 added multilingual casting as part of #10476 --- .../casts/MultilingualSettingAttribute.php | 55 +++++++++++++++++++ classes/core/traits/ModelWithSettings.php | 31 ++++++++--- 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 classes/core/casts/MultilingualSettingAttribute.php diff --git a/classes/core/casts/MultilingualSettingAttribute.php b/classes/core/casts/MultilingualSettingAttribute.php new file mode 100644 index 00000000000..25a0fa8efd3 --- /dev/null +++ b/classes/core/casts/MultilingualSettingAttribute.php @@ -0,0 +1,55 @@ +getMultilingualProps())) { + throw new Exception( + 'Applying multilingual casting on non-maltilingual attribute is not allowed' + ); + } + + if (is_string($value)) { + return [$key => [Locale::getLocale() => $value]]; + } + + return [$key => array_filter($value)]; + } +} \ No newline at end of file diff --git a/classes/core/traits/ModelWithSettings.php b/classes/core/traits/ModelWithSettings.php index 295d61a131c..cecf1f821cf 100644 --- a/classes/core/traits/ModelWithSettings.php +++ b/classes/core/traits/ModelWithSettings.php @@ -21,6 +21,7 @@ use Eloquence\Behaviours\HasCamelCasing; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Casts\Attribute; +use PKP\core\casts\MultilingualSettingAttribute; use PKP\core\maps\Schema; use PKP\core\SettingsBuilder; use PKP\facades\Locale; @@ -78,6 +79,14 @@ public function __construct(array $attributes = []) if (static::getSchemaName()) { $this->setSchemaData(); } else { + $this->generateAttributeCast( + collect($this->getMultilingualProps()) + ->flatMap( + fn (string $attribute): array => [$attribute => MultilingualSettingAttribute::class] + ) + ->toArray() + ); + if (!empty($this->fillable)) { $this->mergeFillable(array_merge($this->getSettings(), $this->getMultilingualProps())); } @@ -163,16 +172,24 @@ protected function setSchemaData(): void protected function convertSchemaToCasts(stdClass $schema): void { $propCast = []; + foreach ($schema->properties as $propName => $propSchema) { - // Don't cast multilingual values as Eloquent tries to convert them from string to arrays with json_decode() - if (isset($propSchema->multilingual)) { - continue; - } - $propCast[$propName] = $propSchema->type; + + $propCast[$propName] = isset($propSchema->multilingual) && $propSchema->multilingual == true + ? MultilingualSettingAttribute::class + : $propSchema->type; } - $propCasts = $this->ensureCastsAreStringValues($propCast); - $this->casts = array_merge($propCasts, $this->casts); + $this->generateAttributeCast($propCast); + } + + /** + * Generate the final cast from dynamically generated attr casts + */ + protected function generateAttributeCast(array $attrCast): void + { + $attrCasts = $this->ensureCastsAreStringValues($attrCast); + $this->casts = array_merge($attrCasts, $this->casts); } /** From edcb7bdd4794eaac90517d850bcd2f404790e8d6 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Wed, 23 Oct 2024 12:30:05 +0600 Subject: [PATCH 19/30] pkp/pkp-lib#10292 fixed issue the data building with settings attrs --- classes/core/SettingsBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/core/SettingsBuilder.php b/classes/core/SettingsBuilder.php index 926878983e9..2d958b0c777 100644 --- a/classes/core/SettingsBuilder.php +++ b/classes/core/SettingsBuilder.php @@ -287,7 +287,7 @@ protected function getModelWithSettings(array|string $columns = ['*']): Collecti $exactRow = $rows->pull($settingModelId); // Even for empty('') locale, the multilingual props need to be an array - if (isset($setting->locale)) { + if (isset($setting->locale) && $this->isMultilingual($setting->setting_name)) { $exactRow->{$setting->setting_name}[$setting->locale] = $setting->setting_value; } else { $exactRow->{$setting->setting_name} = $setting->setting_value; From 87e88072d64af085fc8885d1474e5f887e17db0b Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Sat, 16 Nov 2024 21:15:20 +0600 Subject: [PATCH 20/30] pkp/pkp-lib#10292 updated setting builder to work with non schema based entity --- .../controlledVocab/ControlledVocabEntry.php | 2 +- classes/core/SettingsBuilder.php | 51 +++++++++++++++++-- .../casts/MultilingualSettingAttribute.php | 6 +-- classes/core/traits/EntityUpdate.php | 20 +++++--- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 707de4232d7..07efeca2a22 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -64,7 +64,7 @@ public function getSettingsTable(): string protected function casts(): array { return [ - 'controlled_vocab_entry_id' => 'string', + 'controlled_vocab_entry_id' => 'integer', 'controlled_vocab_id' => 'integer', 'seq' => 'float', ]; diff --git a/classes/core/SettingsBuilder.php b/classes/core/SettingsBuilder.php index 2d958b0c777..301ff326ef8 100644 --- a/classes/core/SettingsBuilder.php +++ b/classes/core/SettingsBuilder.php @@ -29,6 +29,7 @@ class SettingsBuilder extends Builder { use EntityUpdate; + /** * Get the hydrated models without eager loading. * @@ -75,9 +76,27 @@ public function update(array $values) $schema = null; if (!$this->getSchemaName()) { - $casts = $this->model->getCasts(); + + // Casts are always defined in snake key based column name to cast type + // Need to convert the snake key based column names to camel case + $casts = collect($this->model->getCasts())->mapWithKeys( + fn(string $cast, string $columnName): array => [ + Str::camel($columnName) => $cast + ] + )->toArray(); + foreach ($this->model->getSettings() as $settingName) { - $schema['properties'][$settingName]['multilingual'] = in_array($settingName, $this->model->getMultilingualProps()); + + // If this settings column is not intened to update, + // no need to set any type for it + if (!$settingValues->has($settingName)) { + continue; + } + + $schema['properties'][$settingName]['multilingual'] = in_array( + $settingName, + $this->model->getMultilingualProps() + ); if (array_key_exists($settingName, $casts)) { $type = $casts[$settingName]; @@ -88,11 +107,16 @@ public function update(array $values) E_USER_WARNING ); } + $schema['properties'][$settingName]['type'] = $type; } } - $this->updateSettings($settingValues->toArray(), $this->model->getKey(), !is_null($schema) ? json_decode(json_encode($schema)) : null); + $this->updateSettings( + $settingValues->toArray(), + $this->model->getKey(), + !is_null($schema) ? json_decode(json_encode($schema)) : null + ); return $count ?? 0; } @@ -239,11 +263,17 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) return $this; } + /** + * @see \PKP\core\traits\EntityUpdate::getSettingsTable() + */ public function getSettingsTable(): ?string { return $this->model->getSettingsTable(); } + /** + * @see \PKP\core\traits\EntityUpdate::getPrimaryKeyName() + */ public function getPrimaryKeyName(): string { return $this->model->getKeyName(); @@ -257,6 +287,9 @@ public function isSetting(string $settingName): bool return in_array($settingName, $this->model->getSettings()); } + /** + * @see \PKP\core\traits\EntityUpdate::getSchemaName() + */ public function getSchemaName(): ?string { return $this->model->getSchemaName(); @@ -345,20 +378,28 @@ protected function isMultilingual(string $settingName): bool protected function getSettingRows(mixed $settingValues, int $id): array { $rows = []; + $settingValues->each(function (mixed $settingValue, string $settingName) use ($id, &$rows) { $settingName = Str::camel($settingName); if ($this->isMultilingual($settingName)) { foreach ($settingValue as $locale => $localizedValue) { $rows[] = [ - $this->getPrimaryKeyName() => $id, 'locale' => $locale, 'setting_name' => $settingName, 'setting_value' => $localizedValue + $this->getPrimaryKeyName() => $id, + 'locale' => $locale, + 'setting_name' => $settingName, + 'setting_value' => $localizedValue, ]; } } else { $rows[] = [ - $this->getPrimaryKeyName() => $id, 'locale' => '', 'setting_name' => $settingName, 'setting_value' => $settingValue + $this->getPrimaryKeyName() => $id, + 'locale' => '', + 'setting_name' => $settingName, + 'setting_value' => $settingValue, ]; } }); + return $rows; } } diff --git a/classes/core/casts/MultilingualSettingAttribute.php b/classes/core/casts/MultilingualSettingAttribute.php index 25a0fa8efd3..7e424ad1ebd 100644 --- a/classes/core/casts/MultilingualSettingAttribute.php +++ b/classes/core/casts/MultilingualSettingAttribute.php @@ -32,7 +32,7 @@ public function set(Model $model, string $key, mixed $value, array $attributes): if (!in_array(ModelWithSettings::class, class_uses_recursive(get_class($model)))) { throw new Exception( sprintf( - "model class %s does not support multilingual setting attributes/properties", + "Model class %s does not support multilingual setting attributes/properties", get_class($model) ) ); @@ -42,7 +42,7 @@ public function set(Model $model, string $key, mixed $value, array $attributes): if (!in_array($key, $model->getMultilingualProps())) { throw new Exception( - 'Applying multilingual casting on non-maltilingual attribute is not allowed' + "Applying multilingual casting on non-maltilingual attribute {$key} is not allowed" ); } @@ -52,4 +52,4 @@ public function set(Model $model, string $key, mixed $value, array $attributes): return [$key => array_filter($value)]; } -} \ No newline at end of file +} diff --git a/classes/core/traits/EntityUpdate.php b/classes/core/traits/EntityUpdate.php index b80c4b14a2f..8102caf06a5 100644 --- a/classes/core/traits/EntityUpdate.php +++ b/classes/core/traits/EntityUpdate.php @@ -74,7 +74,15 @@ public function updateSettings(array $props, int $modelId, $schema = null): void $deleteSettings[] = $propName; continue; } + if (!empty($propSchema->multilingual)) { + // first we will delete the settings entries for the locale keys which are not present in update query + DB::table($this->getSettingsTable()) + ->where($this->getPrimaryKeyName(), '=', $modelId) + ->where('setting_name', '=', $propName) + ->whereNotIn('locale', array_keys($props[$propName])) + ->delete(); + foreach ($props[$propName] as $localeKey => $localeValue) { // Delete rows with a null value if (is_null($localeValue)) { @@ -92,9 +100,9 @@ public function updateSettings(array $props, int $modelId, $schema = null): void 'setting_name' => $propName, ], [ - 'setting_value' => method_exists($this, 'convertToDB') ? - $this->convertToDB($localeValue, $schema->properties->{$propName}->type) : - $localeValue + 'setting_value' => method_exists($this, 'convertToDB') + ? $this->convertToDB($localeValue, $schema->properties->{$propName}->type) + : $localeValue ] ); } @@ -108,9 +116,9 @@ public function updateSettings(array $props, int $modelId, $schema = null): void 'setting_name' => $propName, ], [ - 'setting_value' => method_exists($this, 'convertToDB') ? - $this->convertToDB($props[$propName], $schema->properties->{$propName}->type) : - $props[$propName] + 'setting_value' => method_exists($this, 'convertToDB') + ? $this->convertToDB($props[$propName], $schema->properties->{$propName}->type) + : $props[$propName] ] ); } From 4ba0abe1e7be705860caf5af45f41983900d5fad Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Sat, 16 Nov 2024 22:48:40 +0600 Subject: [PATCH 21/30] pkp/pkp-lib#10292 allow nullable locales to be passed to repo method getBySymbolic --- classes/controlledVocab/Repository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 5edc4e2cae1..7f5bc7be629 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -44,7 +44,7 @@ public function getBySymbolic( string $symbolic, int $assocType, int $assocId, - array $locales = [] + ?array $locales = [] ): array { $result = []; From 31e9afcd03eacb095705931a3b509678d1ab30b7 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Sun, 24 Nov 2024 16:22:14 +0600 Subject: [PATCH 22/30] pkp/pkp-lib#10292 updated to handle context based query --- api/v1/vocabs/PKPVocabController.php | 4 +- classes/controlledVocab/ControlledVocab.php | 32 +++---- .../controlledVocab/ControlledVocabEntry.php | 85 ++++++++-------- .../ControlledVocabEntryMatch.php | 37 +++++++ classes/controlledVocab/Repository.php | 72 +++++++------- classes/metadata/MetadataProperty.php | 2 +- ...ContextIdColumnToControlledVocabsTable.php | 96 +++++++++++++++++++ classes/user/interest/Repository.php | 79 +++++++-------- classes/user/interest/UserInterest.php | 4 +- 9 files changed, 257 insertions(+), 154 deletions(-) create mode 100644 classes/controlledVocab/ControlledVocabEntryMatch.php create mode 100644 classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 511baa78d02..6906ef61776 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -119,9 +119,9 @@ public function getMany(Request $illuminateRequest): JsonResponse $entries = ControlledVocabEntry::query() ->whereHas( "controlledVocab", - fn($query) => $query->withSymbolic($vocab)->withContextId($context->getId()) + fn ($query) => $query->withSymbolic($vocab)->withContextId($context->getId()) ) - ->withLocale($locale) + ->withLocales([$locale]) ->when($term, fn ($query) => $query->withSetting($vocab, $term)) ->get(); } else { diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index aaadba13d8f..cda7450a200 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -68,6 +68,7 @@ protected function casts(): array 'symbolic' => 'string', 'assoc_type' => 'integer', 'assoc_id' => 'integer', + 'context_id' => 'integer', ]; } @@ -77,8 +78,8 @@ protected function casts(): array protected function id(): Attribute { return Attribute::make( - get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, - set: fn($value) => [$this->primaryKey => $value], + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], )->shouldCache(); } @@ -135,24 +136,15 @@ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Bu */ public function scopeWithContextId(Builder $query, int $contextId): Builder { - return $query - ->where( - fn ($query) => $query - ->select('context_id') - ->from('submissions') - ->whereColumn( - DB::raw( - "(SELECT publications.submission_id - FROM publications - INNER JOIN {$this->table} - ON publications.publication_id = {$this->table}.assoc_id - LIMIT 1)" - ), - '=', - 'submissions.submission_id' - ), - $contextId - ); + return $query->where('context_id', $contextId); + } + + /** + * Scope a query to only include vocabs not associated with any context + */ + public function scopeWithoutContext(Builder $query): Builder + { + return $query->whereNull('context_id'); } /** diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 07efeca2a22..58447e4733d 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -16,13 +16,13 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; use PKP\user\interest\UserInterest; use PKP\core\traits\ModelWithSettings; -use Illuminate\Database\Eloquent\Model; use PKP\controlledVocab\ControlledVocab; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Relations\BelongsTo; +use PKP\controlledVocab\ControlledVocabEntryMatch; class ControlledVocabEntry extends Model { @@ -127,53 +127,50 @@ public function scopeWithControlledVocabId(Builder $query, int $controlledVocabI } /** - * Scope a query to only include entries for a specific locale/s + * Scope a query to only include entries for a specific locales */ - public function scopeWithLocale(Builder $query, string|array $locale): Builder + public function scopeWithLocales(Builder $query, array $locales): Builder { - if (is_array($locale)) { - return $query->whereIn( - DB::raw( - "(SELECT locale - FROM {$this->getSettingsTable()} - WHERE {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} - LIMIT 1)" - ), - $locale - ); - } - - return $query->where( - fn ($query) => $query - ->select("locale") - ->from("{$this->getSettingsTable()}") - ->whereColumn( - "{$this->getSettingsTable()}.{$this->primaryKey}", - "{$this->table}.{$this->primaryKey}" - ) - ->limit(1), - $locale + return $query->whereIn( + DB::raw( + "(SELECT locale + FROM {$this->getSettingsTable()} + WHERE {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} + LIMIT 1)" + ), + $locales ); } /** - * Scope a query to only include entries for a specific setting name and value/s + * Scope a query to only include entries for a specific setting name and values */ - public function scopeWithSetting(Builder $query, string $settingName, string|array $settingValue, bool $partial = true): Builder + public function scopeWithSettings(Builder $query, string $settingName, array $settingValue): Builder { - if (is_array($settingValue)) { - return $query->whereIn( - DB::raw( - "(SELECT setting_value - FROM {$this->getSettingsTable()} - WHERE setting_name = '{$settingName}' - AND {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} - LIMIT 1)" - ), - $settingValue - ); - } + // TODO : Does not work as intended, need modifications + return $query->whereIn( + DB::raw( + "(SELECT setting_value + FROM {$this->getSettingsTable()} + WHERE setting_name = '{$settingName}' + AND {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} + LIMIT 1)" + ), + $settingValue + ); + } + /** + * Scope a query to only include entries for a specific setting name and value with exact or partial match + */ + public function scopeWithSetting( + Builder $query, + string $settingName, + string $settingValue, + ControlledVocabEntryMatch $match = ControlledVocabEntryMatch::PARTIAL + ): Builder + { + // TODO : probably need some modification return $query->where( fn ($query) => $query ->select('setting_value') @@ -184,8 +181,8 @@ public function scopeWithSetting(Builder $query, string $settingName, string|arr "{$this->table}.{$this->primaryKey}" ) ->limit(1), - ($partial ? 'LIKE' : '='), - ($partial ? "%{$settingValue}%" : $settingValue) + ($match->operator()), + ($match->searchKeyword($settingValue)) ); } } diff --git a/classes/controlledVocab/ControlledVocabEntryMatch.php b/classes/controlledVocab/ControlledVocabEntryMatch.php new file mode 100644 index 00000000000..647e4380e81 --- /dev/null +++ b/classes/controlledVocab/ControlledVocabEntryMatch.php @@ -0,0 +1,37 @@ + "=", + static::PARTIAL => "LIKE" + }; + } + + public function searchKeyword(string $keyword): string + { + return match ($this) { + static::EXACT => $keyword, + static::PARTIAL => "%{$keyword}%" + }; + } +} diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 7f5bc7be629..432c1cec964 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -15,25 +15,34 @@ namespace PKP\controlledVocab; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\DB; use PKP\controlledVocab\ControlledVocab; use PKP\controlledVocab\ControlledVocabEntry; -use Throwable; class Repository { /** * Fetch a Controlled Vocab by symbolic info, building it if needed. */ - public function build(string $symbolic, int $assocType = 0, int $assocId = 0): ControlledVocab + public function build( + string $symbolic, + int $assocType = 0, + int $assocId = 0, + ?int $contextId = null + ): ControlledVocab { return ControlledVocab::query() ->withSymbolic($symbolic) ->withAssoc($assocType, $assocId) + ->when( + $contextId, + fn ($query) => $query->withContextId($contextId), + fn ($query) => $query->withoutContext(), + ) ->firstOr(fn() => ControlledVocab::create([ 'assocType' => $assocType, 'assocId' => $assocId, 'symbolic' => $symbolic, + 'contextId' => $contextId, ])); } @@ -54,7 +63,7 @@ public function getBySymbolic( "controlledVocab", fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) ) - ->when(!empty($locales), fn ($query) => $query->withLocale($locales)) + ->when(!empty($locales), fn ($query) => $query->withLocales($locales)) ->get() ->each(function ($entry) use (&$result, $symbolic) { foreach ($entry->{$symbolic} as $locale => $value) { @@ -79,42 +88,29 @@ public function insertBySymbolic( $controlledVocab = $this->build($symbolic, $assocType, $assocId); $controlledVocab->load('controlledVocabEntries'); - try { - - DB::beginTransaction(); + if ($deleteFirst) { + ControlledVocabEntry::query() + ->whereIn( + (new ControlledVocabEntry)->getKeyName(), + $controlledVocab->controlledVocabEntries->pluck('id')->toArray() + ) + ->delete(); + } - if ($deleteFirst) { - ControlledVocabEntry::query() - ->whereIn( - (new ControlledVocabEntry)->getKeyName(), - $controlledVocab->controlledVocabEntries->pluck('id')->toArray() + collect($vocabs) + ->each( + fn (array|string $entries, string $locale) => collect(array_values(Arr::wrap($entries))) + ->each( + fn (string $vocab, int $index) => + ControlledVocabEntry::create([ + 'controlledVocabId' => $controlledVocab->id, + 'seq' => $index + 1, + "{$symbolic}" => [ + $locale => $vocab + ], + ]) ) - ->delete(); - } - - collect($vocabs) - ->each( - fn (array|string $entries, string $locale) => collect(array_values(Arr::wrap($entries))) - ->each( - fn (string $vocab, int $index) => - ControlledVocabEntry::create([ - 'controlledVocabId' => $controlledVocab->id, - 'seq' => $index + 1, - "{$symbolic}" => [ - $locale => $vocab - ], - ]) - ) - ); - - DB::commit(); - - } catch (Throwable $exception) { - - DB::rollBack(); - - throw $exception; - } + ); } /** diff --git a/classes/metadata/MetadataProperty.php b/classes/metadata/MetadataProperty.php index ba15cd9007c..6598aabbfb2 100644 --- a/classes/metadata/MetadataProperty.php +++ b/classes/metadata/MetadataProperty.php @@ -428,7 +428,7 @@ public function isValid($value, $locale = null) 'controlledVocab', fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) ) - ->withLocale($locale) + ->withLocales([$locale]) ->withSetting($symbolic, $value) ->first(); diff --git a/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php b/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php new file mode 100644 index 00000000000..28cc3a6cef8 --- /dev/null +++ b/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php @@ -0,0 +1,96 @@ +bigInteger('context_id') + ->nullable() + ->comment('Context to which the controlled vocab is associated'); + + $table + ->foreign('context_id') + ->references($this->getContextPrimaryKey()) + ->on($this->getContextTable()) + ->onDelete('cascade'); + + $table->index(['context_id'], 'controlled_vocabs_context_id'); + }); + + DB::table("controlled_vocabs") + ->select(["assoc_id"]) + ->addSelect([ + "context_id" => DB::table("submissions") + ->select("context_id") + ->whereColumn( + DB::raw( + "(SELECT publications.submission_id + FROM publications + INNER JOIN controlled_vocabs + ON publications.publication_id = controlled_vocabs.assoc_id + LIMIT 1)" + ), + "=", + "submissions.submission_id" + ) + ]) + ->whereNot("assoc_type", 0) + ->whereNot("assoc_id", 0) + ->get() + ->groupBy("context_id") + ->each ( + fn (Collection $assocs, int $contextId) => $assocs + ->chunk(1000) + ->each( + fn ($chunkAssocs) => DB::table('controlled_vocabs') + ->whereIn('assoc_id', $chunkAssocs->pluck('assoc_id')->toArray()) + ->update(['context_id' => $contextId]) + ) + ); + } + + /** + * Reverse the migration. + */ + public function down(): void + { + Schema::table('controlled_vocabs', function (Blueprint $table) { + $table->dropColumn(['context_id']); + }); + } +} diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 59ce5efb12c..875db3a2616 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -15,11 +15,9 @@ namespace PKP\user\interest; use APP\facades\Repo; -use Illuminate\Support\Facades\DB; use PKP\user\User; use PKP\user\interest\UserInterest; use PKP\controlledVocab\ControlledVocabEntry; -use Throwable; class Repository { @@ -36,7 +34,7 @@ public function getAllInterests(?string $filter = null): array ->withControlledVocabId($controlledVocab->id) ->when( $filter, - fn($query) => $query->withSetting( + fn ($query) => $query->withSetting( UserInterest::CONTROLLED_VOCAB_INTEREST, $filter ) @@ -55,11 +53,11 @@ public function getInterestsForUser(User $user): array return ControlledVocabEntry::query() ->whereHas( "controlledVocab", - fn($query) => $query + fn ($query) => $query ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) ->withAssoc(0, 0) ) - ->whereHas("userInterest", fn($query) => $query->withUserId($user->getId())) + ->whereHas("userInterest", fn ($query) => $query->withUserId($user->getId())) ->get() ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST, 'id') ->toArray(); @@ -91,54 +89,41 @@ public function setInterestsForUser(User $user, string|array|null $interests = n $currentInterests = ControlledVocabEntry::query() ->whereHas( 'controlledVocab', - fn($query) => $query + fn ($query) => $query ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) ->withAssoc(0, 0) ) - ->withLocale('') - ->withSetting(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) + ->withLocales(['']) + ->withSettings(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) ->get(); - try { - - DB::beginTransaction(); - - // Delete the existing interests association. - UserInterest::query()->withUserId($user->getId())->delete(); + // Delete the existing interests association. + UserInterest::query()->withUserId($user->getId())->delete(); - $newInterestIds = collect( - array_diff( - $interests, - $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->toArray() - ) + $newInterestIds = collect( + array_diff( + $interests, + $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->toArray() ) - ->map(fn (string $interest): string => trim($interest)) - ->unique() - ->map( - fn (string $interest) => ControlledVocabEntry::create([ - 'controlledVocabId' => $controlledVocab->id, - UserInterest::CONTROLLED_VOCAB_INTEREST => [ - '' => $interest - ], - ])->id - ); - - collect($currentInterests->pluck('id')) - ->merge($newInterestIds) - ->each(fn ($interestId) => UserInterest::create([ - 'userId' => $user->getId(), - 'controlledVocabEntryId' => $interestId, - ])); - - Repo::controlledVocab()->resequence($controlledVocab->id); - - DB::commit(); - - } catch (Throwable $exception) { - - DB::rollBack(); - - throw $exception; - } + ) + ->map(fn (string $interest): string => trim($interest)) + ->unique() + ->map( + fn (string $interest) => ControlledVocabEntry::create([ + 'controlledVocabId' => $controlledVocab->id, + UserInterest::CONTROLLED_VOCAB_INTEREST => [ + '' => $interest + ], + ])->id + ); + + collect($currentInterests->pluck('id')) + ->merge($newInterestIds) + ->each(fn ($interestId) => UserInterest::create([ + 'userId' => $user->getId(), + 'controlledVocabEntryId' => $interestId, + ])); + + Repo::controlledVocab()->resequence($controlledVocab->id); } } diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index 97961b45ce0..e3998715cb9 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -67,8 +67,8 @@ protected function casts(): array protected function id(): Attribute { return Attribute::make( - get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null, - set: fn($value) => [$this->primaryKey => $value], + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], )->shouldCache(); } From 96b6fba1cb58495a546ff6847f2933e9b7f9d428 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 25 Nov 2024 00:07:09 +0600 Subject: [PATCH 23/30] pkp/pkp-lib#10292 make context id required on controlled vocabs operations --- classes/controlledVocab/ControlledVocab.php | 18 +++---- classes/controlledVocab/Repository.php | 15 +++--- .../install/ControlledVocabMigration.php | 24 ++++++++- classes/publication/DAO.php | 52 +++++++------------ classes/user/interest/Repository.php | 10 +++- classes/user/interest/UserInterest.php | 3 ++ .../filter/NativeXmlPKPPublicationFilter.php | 2 + tests/PKPTestHelper.php | 21 ++++---- .../FormValidatorControlledVocabTest.php | 3 +- .../classes/metadata/MetadataPropertyTest.php | 2 +- .../ValidatorControlledVocabTest.php | 3 +- 11 files changed, 85 insertions(+), 68 deletions(-) diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index cda7450a200..d826816d980 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -121,6 +121,14 @@ public function scopeWithSymbolic(Builder $query, string $symbolic): Builder return $query->where('symbolic', $symbolic); } + /** + * Scope a query to only include vocabs with a specific symbolic. + */ + public function scopeWithSymbolics(Builder $query, array $symbolics): Builder + { + return $query->whereIn('symbolic', $symbolics); + } + /** * Scope a query to only include vocabs with a specific assoc type and assoc ID. */ @@ -134,19 +142,11 @@ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Bu /** * Scope a query to only include vocabs associated with given context id */ - public function scopeWithContextId(Builder $query, int $contextId): Builder + public function scopeWithContextId(Builder $query, ?int $contextId): Builder { return $query->where('context_id', $contextId); } - /** - * Scope a query to only include vocabs not associated with any context - */ - public function scopeWithoutContext(Builder $query): Builder - { - return $query->whereNull('context_id'); - } - /** * Get a list of controlled vocabulary options. * diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 432c1cec964..757f550e475 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -25,19 +25,15 @@ class Repository */ public function build( string $symbolic, - int $assocType = 0, - int $assocId = 0, - ?int $contextId = null + int $assocType, + int $assocId, + ?int $contextId ): ControlledVocab { return ControlledVocab::query() ->withSymbolic($symbolic) ->withAssoc($assocType, $assocId) - ->when( - $contextId, - fn ($query) => $query->withContextId($contextId), - fn ($query) => $query->withoutContext(), - ) + ->withContextId($contextId) ->firstOr(fn() => ControlledVocab::create([ 'assocType' => $assocType, 'assocId' => $assocId, @@ -82,10 +78,11 @@ public function insertBySymbolic( array $vocabs, int $assocType, int $assocId, + ?int $contextId, bool $deleteFirst = true, ): void { - $controlledVocab = $this->build($symbolic, $assocType, $assocId); + $controlledVocab = $this->build($symbolic, $assocType, $assocId, $contextId); $controlledVocab->load('controlledVocabEntries'); if ($deleteFirst) { diff --git a/classes/migration/install/ControlledVocabMigration.php b/classes/migration/install/ControlledVocabMigration.php index 431184bf15a..e3506c45fa1 100644 --- a/classes/migration/install/ControlledVocabMigration.php +++ b/classes/migration/install/ControlledVocabMigration.php @@ -17,8 +17,18 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class ControlledVocabMigration extends \PKP\migration\Migration +abstract class ControlledVocabMigration extends \PKP\migration\Migration { + /** + * Name of the context table + */ + abstract protected function getContextTable(): string; + + /** + * Name of the context table primary key + */ + abstract protected function getContextPrimaryKey(): string; + /** * Run the migrations. */ @@ -31,6 +41,18 @@ public function up(): void $table->string('symbolic', 64); $table->bigInteger('assoc_type')->default(0); $table->bigInteger('assoc_id')->default(0); + + $table + ->bigInteger('context_id') + ->nullable() + ->comment('Context to which the controlled vocab is associated'); + $table + ->foreign('context_id') + ->references($this->getContextPrimaryKey()) + ->on($this->getContextTable()) + ->onDelete('cascade'); + $table->index(['context_id'], 'controlled_vocabs_context_id'); + $table->unique(['symbolic', 'assoc_type', 'assoc_id'], 'controlled_vocab_symbolic'); }); diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 5fe580eed84..3906a65701d 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -186,11 +186,14 @@ public function fromRow(object $row): Publication */ public function insert(Publication $publication): int { + $contextId = Repo::submission()->get( + $publication->getData('submissionId') + )->getData('contextId'); $vocabs = $this->extractControlledVocab($publication); $id = parent::_insert($publication); - $this->saveControlledVocab($vocabs, $id); + $this->saveControlledVocab($vocabs, $id, $contextId); $this->saveCategories($publication); // Parse the citations @@ -206,11 +209,15 @@ public function insert(Publication $publication): int */ public function update(Publication $publication, ?Publication $oldPublication = null) { + $contextId = Repo::submission()->get( + $publication->getData('submissionId') + )->getData('contextId'); + $vocabs = $this->extractControlledVocab($publication); parent::_update($publication); - $this->saveControlledVocab($vocabs, $publication->getId()); + $this->saveControlledVocab($vocabs, $publication->getId(), $contextId); $this->saveCategories($publication); if ($oldPublication && $oldPublication->getData('citationsRaw') != $publication->getData('citationsRaw')) { @@ -413,15 +420,15 @@ protected function extractControlledVocab(Publication $publication): array * * @see self::extractControlledVocab() */ - protected function saveControlledVocab(array $values, int $publicationId) + protected function saveControlledVocab(array $values, int $publicationId, int $contextId) { // Update controlled vocabularly for which we have props foreach ($values as $prop => $value) { match ($prop) { - 'keywords' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), - 'subjects' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), - 'disciplines' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), - 'supportingAgencies' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'keywords' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), + 'subjects' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), + 'disciplines' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), + 'supportingAgencies' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), }; } } @@ -431,33 +438,10 @@ protected function saveControlledVocab(array $values, int $publicationId) */ protected function deleteControlledVocab(int $publicationId) { - Repo::controlledVocab()->insertBySymbolic( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, - [], - Application::ASSOC_TYPE_PUBLICATION, - $publicationId - ); - - Repo::controlledVocab()->insertBySymbolic( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, - [], - Application::ASSOC_TYPE_PUBLICATION, - $publicationId - ); - - Repo::controlledVocab()->insertBySymbolic( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, - [], - Application::ASSOC_TYPE_PUBLICATION, - $publicationId - ); - - Repo::controlledVocab()->insertBySymbolic( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, - [], - Application::ASSOC_TYPE_PUBLICATION, - $publicationId - ); + ControlledVocab::query() + ->withSymbolics(ControlledVocab::getDefinedVocabSymbolic()) + ->withAssoc(Application::ASSOC_TYPE_PUBLICATION, $publicationId) + ->delete(); } /** diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 875db3a2616..dc1a262b682 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -27,7 +27,10 @@ class Repository public function getAllInterests(?string $filter = null): array { $controlledVocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST + UserInterest::CONTROLLED_VOCAB_INTEREST, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, + UserInterest::CONTROLLED_VOCAB_INTEREST_CONTEXT_ID ); return ControlledVocabEntry::query() @@ -83,7 +86,10 @@ public function setInterestsForUser(User $user, string|array|null $interests = n : (empty($interests) ? [] : explode(',', $interests)); $controlledVocab = Repo::controlledVocab()->build( - UserInterest::CONTROLLED_VOCAB_INTEREST + UserInterest::CONTROLLED_VOCAB_INTEREST, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, + UserInterest::CONTROLLED_VOCAB_INTEREST_CONTEXT_ID ); $currentInterests = ControlledVocabEntry::query() diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index e3998715cb9..a5645a36c48 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -27,6 +27,9 @@ class UserInterest extends Model use HasCamelCasing; public const CONTROLLED_VOCAB_INTEREST = 'interest'; + public const CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE = 0; + public const CONTROLLED_VOCAB_INTEREST_ASSOC_ID = 0; + public const CONTROLLED_VOCAB_INTEREST_CONTEXT_ID = null; /** * @copydoc \Illuminate\Database\Eloquent\Model::$table diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index db84fff1e15..22f53e7eca7 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -128,6 +128,7 @@ public function populateObject($publication, $node) */ public function handleChildElement($n, $publication) { + $submission = $this->getDeployment()->getSubmission(); $setterMappings = $this->_getLocalizedPublicationFields(); $controlledVocabulariesMappings = $this->_getControlledVocabulariesMappings(); @@ -156,6 +157,7 @@ public function handleChildElement($n, $publication) $controlledVocabulariesValues, Application::ASSOC_TYPE_PUBLICATION, $publication->getId(), + $submission->getData('contextId'), false ); diff --git a/tests/PKPTestHelper.php b/tests/PKPTestHelper.php index 885ac1c4405..e947e9c981b 100644 --- a/tests/PKPTestHelper.php +++ b/tests/PKPTestHelper.php @@ -104,16 +104,17 @@ public static function restoreDB($test) case 'mysql': case 'mariadb': exec( - $cmd = 'zcat ' . - escapeshellarg($filename) . - ' | /usr/bin/mysql --user=' . - escapeshellarg(Config::getVar('database', 'username')) . - ' --password=' . - escapeshellarg(Config::getVar('database', 'password')) . - ' --host=' . - escapeshellarg(Config::getVar('database', 'host')) . - ' ' . - escapeshellarg(Config::getVar('database', 'name')), + $cmd = 'mysql -u ' .Config::getVar('database', 'username'). ' -h ' . Config::getVar('database', 'host') . ' ' . Config::getVar('database', 'name'). ' < ' . $filename, + // $cmd = 'zcat ' . + // escapeshellarg($filename) . + // ' | /usr/bin/mysql --user=' . + // escapeshellarg(Config::getVar('database', 'username')) . + // ' --password=' . + // escapeshellarg(Config::getVar('database', 'password')) . + // ' --host=' . + // escapeshellarg(Config::getVar('database', 'host')) . + // ' ' . + // escapeshellarg(Config::getVar('database', 'name')), $output, $status ); diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index c837efe9bb9..b70e60a3bc4 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -42,7 +42,8 @@ public function testIsValid() $testControlledVocab = Repo::controlledVocab()->build( ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - $assocId + $assocId, + null ); $controlledVocabEntryId1 = ControlledVocabEntry::create([ diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index 7e71fd19f8a..7075e295251 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -148,7 +148,7 @@ public function testValidateControlledVocabulary() { // Build a test vocabulary. (Assoc type and id are 0 to simulate a site-wide vocabulary). $vocab = Repo::controlledVocab()->build( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 0, 0 + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 0, 0, null ); $controlledVocabEntry = ControlledVocabEntry::create([ diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index 3b760a8ccd7..bb55f8ec812 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -37,7 +37,8 @@ public function testValidatorControlledVocab() $testControlledVocab = Repo::controlledVocab()->build( ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - $assocId + $assocId, + null ); $controlledVocabEntryId1 = ControlledVocabEntry::create([ From aeeb424a68679c8454db9edf5fc8556c81fafdef Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 25 Nov 2024 00:35:46 +0600 Subject: [PATCH 24/30] pkp/pkp-lib#10292 update mistaken test modification --- .../controlledVocab/ControlledVocabEntry.php | 4 ++-- tests/PKPTestHelper.php | 21 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 58447e4733d..33e74a7c64c 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -131,6 +131,7 @@ public function scopeWithControlledVocabId(Builder $query, int $controlledVocabI */ public function scopeWithLocales(Builder $query, array $locales): Builder { + // TODO : Does not work as intended, need modifications return $query->whereIn( DB::raw( "(SELECT locale @@ -147,7 +148,7 @@ public function scopeWithLocales(Builder $query, array $locales): Builder */ public function scopeWithSettings(Builder $query, string $settingName, array $settingValue): Builder { - // TODO : Does not work as intended, need modifications + // TODO : probably need some modification return $query->whereIn( DB::raw( "(SELECT setting_value @@ -170,7 +171,6 @@ public function scopeWithSetting( ControlledVocabEntryMatch $match = ControlledVocabEntryMatch::PARTIAL ): Builder { - // TODO : probably need some modification return $query->where( fn ($query) => $query ->select('setting_value') diff --git a/tests/PKPTestHelper.php b/tests/PKPTestHelper.php index e947e9c981b..885ac1c4405 100644 --- a/tests/PKPTestHelper.php +++ b/tests/PKPTestHelper.php @@ -104,17 +104,16 @@ public static function restoreDB($test) case 'mysql': case 'mariadb': exec( - $cmd = 'mysql -u ' .Config::getVar('database', 'username'). ' -h ' . Config::getVar('database', 'host') . ' ' . Config::getVar('database', 'name'). ' < ' . $filename, - // $cmd = 'zcat ' . - // escapeshellarg($filename) . - // ' | /usr/bin/mysql --user=' . - // escapeshellarg(Config::getVar('database', 'username')) . - // ' --password=' . - // escapeshellarg(Config::getVar('database', 'password')) . - // ' --host=' . - // escapeshellarg(Config::getVar('database', 'host')) . - // ' ' . - // escapeshellarg(Config::getVar('database', 'name')), + $cmd = 'zcat ' . + escapeshellarg($filename) . + ' | /usr/bin/mysql --user=' . + escapeshellarg(Config::getVar('database', 'username')) . + ' --password=' . + escapeshellarg(Config::getVar('database', 'password')) . + ' --host=' . + escapeshellarg(Config::getVar('database', 'host')) . + ' ' . + escapeshellarg(Config::getVar('database', 'name')), $output, $status ); From 10b6b063420fc3696e3f9e165df2c7057ffc9657 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Mon, 25 Nov 2024 02:27:06 +0600 Subject: [PATCH 25/30] pkp/pkp-lib#10292 sub query update for locale and setting value search --- .../controlledVocab/ControlledVocabEntry.php | 65 +++++++++++-------- classes/user/interest/Repository.php | 10 ++- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 33e74a7c64c..7ab3b9c4410 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -131,15 +131,15 @@ public function scopeWithControlledVocabId(Builder $query, int $controlledVocabI */ public function scopeWithLocales(Builder $query, array $locales): Builder { - // TODO : Does not work as intended, need modifications - return $query->whereIn( - DB::raw( - "(SELECT locale - FROM {$this->getSettingsTable()} - WHERE {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} - LIMIT 1)" - ), - $locales + return $query->whereExists( + fn ($query) => $query + ->select("{$this->primaryKey}") + ->from("{$this->getSettingsTable()}") + ->whereColumn( + "{$this->getSettingsTable()}.{$this->primaryKey}", + "{$this->getTable()}.{$this->primaryKey}" + ) + ->whereIn(DB::raw("{$this->getSettingsTable()}.locale"), $locales) ); } @@ -148,16 +148,23 @@ public function scopeWithLocales(Builder $query, array $locales): Builder */ public function scopeWithSettings(Builder $query, string $settingName, array $settingValue): Builder { - // TODO : probably need some modification - return $query->whereIn( - DB::raw( - "(SELECT setting_value - FROM {$this->getSettingsTable()} - WHERE setting_name = '{$settingName}' - AND {$this->getSettingsTable()}.{$this->primaryKey} = {$this->table}.{$this->primaryKey} - LIMIT 1)" - ), - $settingValue + return $query->whereExists( + fn ($query) => $query + ->select("{$this->primaryKey}") + ->from("{$this->getSettingsTable()}") + ->whereColumn( + "{$this->getSettingsTable()}.{$this->primaryKey}", + "{$this->getTable()}.{$this->primaryKey}" + ) + ->where( + "{$this->getSettingsTable()}.setting_name", + '=', + $settingName + ) + ->whereIn( + DB::raw("{$this->getSettingsTable()}.setting_value"), + $settingValue + ) ); } @@ -171,18 +178,24 @@ public function scopeWithSetting( ControlledVocabEntryMatch $match = ControlledVocabEntryMatch::PARTIAL ): Builder { - return $query->where( + return $query->whereExists( fn ($query) => $query - ->select('setting_value') + ->select("{$this->primaryKey}") ->from("{$this->getSettingsTable()}") - ->where('setting_name', $settingName) ->whereColumn( "{$this->getSettingsTable()}.{$this->primaryKey}", - "{$this->table}.{$this->primaryKey}" + "{$this->getTable()}.{$this->primaryKey}" + ) + ->where( + "{$this->getSettingsTable()}.setting_name", + '=', + $settingName + ) + ->where( + "{$this->getSettingsTable()}.setting_value", + $match->operator(), + $match->searchKeyword($settingValue) ) - ->limit(1), - ($match->operator()), - ($match->searchKeyword($settingValue)) ); } } diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index dc1a262b682..8a966de58f1 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -58,7 +58,10 @@ public function getInterestsForUser(User $user): array "controlledVocab", fn ($query) => $query ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->withAssoc(0, 0) + ->withAssoc( + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID + ) ) ->whereHas("userInterest", fn ($query) => $query->withUserId($user->getId())) ->get() @@ -97,7 +100,10 @@ public function setInterestsForUser(User $user, string|array|null $interests = n 'controlledVocab', fn ($query) => $query ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->withAssoc(0, 0) + ->withAssoc( + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, + UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID + ) ) ->withLocales(['']) ->withSettings(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) From 5649196b2387fedf45d56c2c3f018ec2f15412f6 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Fri, 29 Nov 2024 12:56:31 +0600 Subject: [PATCH 26/30] pkp/pkp-lib#10292 revert back context id direct addition to vocabs --- api/v1/vocabs/PKPVocabController.php | 2 +- classes/controlledVocab/ControlledVocab.php | 34 ++++--- classes/controlledVocab/Repository.php | 12 +-- classes/metadata/MetadataProperty.php | 2 +- .../install/ControlledVocabMigration.php | 24 +---- ...ContextIdColumnToControlledVocabsTable.php | 96 ------------------- classes/publication/DAO.php | 21 ++-- classes/user/interest/Repository.php | 6 +- classes/user/interest/UserInterest.php | 1 - .../validation/ValidatorControlledVocab.php | 2 +- .../filter/NativeXmlPKPPublicationFilter.php | 2 - .../FormValidatorControlledVocabTest.php | 3 +- .../classes/metadata/MetadataPropertyTest.php | 2 +- .../ValidatorControlledVocabTest.php | 3 +- 14 files changed, 40 insertions(+), 170 deletions(-) delete mode 100644 classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 6906ef61776..27ffd343682 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -119,7 +119,7 @@ public function getMany(Request $illuminateRequest): JsonResponse $entries = ControlledVocabEntry::query() ->whereHas( "controlledVocab", - fn ($query) => $query->withSymbolic($vocab)->withContextId($context->getId()) + fn ($query) => $query->withSymbolics([$vocab])->withContextId($context->getId()) ) ->withLocales([$locale]) ->when($term, fn ($query) => $query->withSetting($vocab, $term)) diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index d826816d980..ab2f4527c43 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -68,7 +68,6 @@ protected function casts(): array 'symbolic' => 'string', 'assoc_type' => 'integer', 'assoc_id' => 'integer', - 'context_id' => 'integer', ]; } @@ -113,14 +112,6 @@ public function controlledVocabEntries(): HasMany return $this->hasMany(ControlledVocabEntry::class, 'controlled_vocab_id', 'controlled_vocab_id'); } - /** - * Scope a query to only include vocabs with a specific symbolic. - */ - public function scopeWithSymbolic(Builder $query, string $symbolic): Builder - { - return $query->where('symbolic', $symbolic); - } - /** * Scope a query to only include vocabs with a specific symbolic. */ @@ -134,17 +125,32 @@ public function scopeWithSymbolics(Builder $query, array $symbolics): Builder */ public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Builder { - return $query - ->where('assoc_type', $assocType) - ->where('assoc_id', $assocId); + return $query->where('assoc_type', $assocType)->where('assoc_id', $assocId); } /** * Scope a query to only include vocabs associated with given context id */ - public function scopeWithContextId(Builder $query, ?int $contextId): Builder + public function scopeWithContextId(Builder $query, int $contextId): Builder { - return $query->where('context_id', $contextId); + return $query + ->where( + fn ($query) => $query + ->select('context_id') + ->from('submissions') + ->whereColumn( + DB::raw( + "(SELECT publications.submission_id + FROM publications + INNER JOIN {$this->table} + ON publications.publication_id = {$this->table}.assoc_id + LIMIT 1)" + ), + '=', + 'submissions.submission_id' + ), + $contextId + ); } /** diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 757f550e475..fa1a7d3db0e 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -26,19 +26,16 @@ class Repository public function build( string $symbolic, int $assocType, - int $assocId, - ?int $contextId + int $assocId ): ControlledVocab { return ControlledVocab::query() - ->withSymbolic($symbolic) + ->withSymbolics([$symbolic]) ->withAssoc($assocType, $assocId) - ->withContextId($contextId) ->firstOr(fn() => ControlledVocab::create([ 'assocType' => $assocType, 'assocId' => $assocId, 'symbolic' => $symbolic, - 'contextId' => $contextId, ])); } @@ -57,7 +54,7 @@ public function getBySymbolic( ControlledVocabEntry::query() ->whereHas( "controlledVocab", - fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) + fn ($query) => $query->withSymbolics([$symbolic])->withAssoc($assocType, $assocId) ) ->when(!empty($locales), fn ($query) => $query->withLocales($locales)) ->get() @@ -78,11 +75,10 @@ public function insertBySymbolic( array $vocabs, int $assocType, int $assocId, - ?int $contextId, bool $deleteFirst = true, ): void { - $controlledVocab = $this->build($symbolic, $assocType, $assocId, $contextId); + $controlledVocab = $this->build($symbolic, $assocType, $assocId); $controlledVocab->load('controlledVocabEntries'); if ($deleteFirst) { diff --git a/classes/metadata/MetadataProperty.php b/classes/metadata/MetadataProperty.php index 6598aabbfb2..9c18aaa091f 100644 --- a/classes/metadata/MetadataProperty.php +++ b/classes/metadata/MetadataProperty.php @@ -426,7 +426,7 @@ public function isValid($value, $locale = null) $entry = ControlledVocabEntry::query() ->whereHas( 'controlledVocab', - fn ($query) => $query->withSymbolic($symbolic)->withAssoc($assocType, $assocId) + fn ($query) => $query->withSymbolics([$symbolic])->withAssoc($assocType, $assocId) ) ->withLocales([$locale]) ->withSetting($symbolic, $value) diff --git a/classes/migration/install/ControlledVocabMigration.php b/classes/migration/install/ControlledVocabMigration.php index e3506c45fa1..431184bf15a 100644 --- a/classes/migration/install/ControlledVocabMigration.php +++ b/classes/migration/install/ControlledVocabMigration.php @@ -17,18 +17,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -abstract class ControlledVocabMigration extends \PKP\migration\Migration +class ControlledVocabMigration extends \PKP\migration\Migration { - /** - * Name of the context table - */ - abstract protected function getContextTable(): string; - - /** - * Name of the context table primary key - */ - abstract protected function getContextPrimaryKey(): string; - /** * Run the migrations. */ @@ -41,18 +31,6 @@ public function up(): void $table->string('symbolic', 64); $table->bigInteger('assoc_type')->default(0); $table->bigInteger('assoc_id')->default(0); - - $table - ->bigInteger('context_id') - ->nullable() - ->comment('Context to which the controlled vocab is associated'); - $table - ->foreign('context_id') - ->references($this->getContextPrimaryKey()) - ->on($this->getContextTable()) - ->onDelete('cascade'); - $table->index(['context_id'], 'controlled_vocabs_context_id'); - $table->unique(['symbolic', 'assoc_type', 'assoc_id'], 'controlled_vocab_symbolic'); }); diff --git a/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php b/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php deleted file mode 100644 index 28cc3a6cef8..00000000000 --- a/classes/migration/upgrade/v3_5_0/I10292_AddContextIdColumnToControlledVocabsTable.php +++ /dev/null @@ -1,96 +0,0 @@ -bigInteger('context_id') - ->nullable() - ->comment('Context to which the controlled vocab is associated'); - - $table - ->foreign('context_id') - ->references($this->getContextPrimaryKey()) - ->on($this->getContextTable()) - ->onDelete('cascade'); - - $table->index(['context_id'], 'controlled_vocabs_context_id'); - }); - - DB::table("controlled_vocabs") - ->select(["assoc_id"]) - ->addSelect([ - "context_id" => DB::table("submissions") - ->select("context_id") - ->whereColumn( - DB::raw( - "(SELECT publications.submission_id - FROM publications - INNER JOIN controlled_vocabs - ON publications.publication_id = controlled_vocabs.assoc_id - LIMIT 1)" - ), - "=", - "submissions.submission_id" - ) - ]) - ->whereNot("assoc_type", 0) - ->whereNot("assoc_id", 0) - ->get() - ->groupBy("context_id") - ->each ( - fn (Collection $assocs, int $contextId) => $assocs - ->chunk(1000) - ->each( - fn ($chunkAssocs) => DB::table('controlled_vocabs') - ->whereIn('assoc_id', $chunkAssocs->pluck('assoc_id')->toArray()) - ->update(['context_id' => $contextId]) - ) - ); - } - - /** - * Reverse the migration. - */ - public function down(): void - { - Schema::table('controlled_vocabs', function (Blueprint $table) { - $table->dropColumn(['context_id']); - }); - } -} diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 3906a65701d..5706876e4eb 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -186,14 +186,11 @@ public function fromRow(object $row): Publication */ public function insert(Publication $publication): int { - $contextId = Repo::submission()->get( - $publication->getData('submissionId') - )->getData('contextId'); $vocabs = $this->extractControlledVocab($publication); $id = parent::_insert($publication); - $this->saveControlledVocab($vocabs, $id, $contextId); + $this->saveControlledVocab($vocabs, $id); $this->saveCategories($publication); // Parse the citations @@ -209,15 +206,11 @@ public function insert(Publication $publication): int */ public function update(Publication $publication, ?Publication $oldPublication = null) { - $contextId = Repo::submission()->get( - $publication->getData('submissionId') - )->getData('contextId'); - $vocabs = $this->extractControlledVocab($publication); parent::_update($publication); - $this->saveControlledVocab($vocabs, $publication->getId(), $contextId); + $this->saveControlledVocab($vocabs, $publication->getId()); $this->saveCategories($publication); if ($oldPublication && $oldPublication->getData('citationsRaw') != $publication->getData('citationsRaw')) { @@ -420,15 +413,15 @@ protected function extractControlledVocab(Publication $publication): array * * @see self::extractControlledVocab() */ - protected function saveControlledVocab(array $values, int $publicationId, int $contextId) + protected function saveControlledVocab(array $values, int $publicationId) { // Update controlled vocabularly for which we have props foreach ($values as $prop => $value) { match ($prop) { - 'keywords' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), - 'subjects' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), - 'disciplines' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), - 'supportingAgencies' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId, $contextId), + 'keywords' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'subjects' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'disciplines' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), + 'supportingAgencies' => Repo::controlledVocab()->insertBySymbolic(ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $value, Application::ASSOC_TYPE_PUBLICATION, $publicationId), }; } } diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 8a966de58f1..80b6129e043 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -30,7 +30,6 @@ public function getAllInterests(?string $filter = null): array UserInterest::CONTROLLED_VOCAB_INTEREST, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, - UserInterest::CONTROLLED_VOCAB_INTEREST_CONTEXT_ID ); return ControlledVocabEntry::query() @@ -57,7 +56,7 @@ public function getInterestsForUser(User $user): array ->whereHas( "controlledVocab", fn ($query) => $query - ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID @@ -92,14 +91,13 @@ public function setInterestsForUser(User $user, string|array|null $interests = n UserInterest::CONTROLLED_VOCAB_INTEREST, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, - UserInterest::CONTROLLED_VOCAB_INTEREST_CONTEXT_ID ); $currentInterests = ControlledVocabEntry::query() ->whereHas( 'controlledVocab', fn ($query) => $query - ->withSymbolic(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index a5645a36c48..c6de5a76898 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -29,7 +29,6 @@ class UserInterest extends Model public const CONTROLLED_VOCAB_INTEREST = 'interest'; public const CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE = 0; public const CONTROLLED_VOCAB_INTEREST_ASSOC_ID = 0; - public const CONTROLLED_VOCAB_INTEREST_CONTEXT_ID = null; /** * @copydoc \Illuminate\Database\Eloquent\Model::$table diff --git a/classes/validation/ValidatorControlledVocab.php b/classes/validation/ValidatorControlledVocab.php index c6c30cd62ba..0b3efa8e4f4 100644 --- a/classes/validation/ValidatorControlledVocab.php +++ b/classes/validation/ValidatorControlledVocab.php @@ -30,7 +30,7 @@ public function __construct(string $symbolic, int $assocType, int $assocId) { /** @var ControlledVocab $controlledVocab */ $controlledVocab = ControlledVocab::query() - ->withSymbolic($symbolic) + ->withSymbolics([$symbolic]) ->withAssoc($assocType, $assocId) ->first(); diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index 22f53e7eca7..db84fff1e15 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -128,7 +128,6 @@ public function populateObject($publication, $node) */ public function handleChildElement($n, $publication) { - $submission = $this->getDeployment()->getSubmission(); $setterMappings = $this->_getLocalizedPublicationFields(); $controlledVocabulariesMappings = $this->_getControlledVocabulariesMappings(); @@ -157,7 +156,6 @@ public function handleChildElement($n, $publication) $controlledVocabulariesValues, Application::ASSOC_TYPE_PUBLICATION, $publication->getId(), - $submission->getData('contextId'), false ); diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index b70e60a3bc4..c837efe9bb9 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -42,8 +42,7 @@ public function testIsValid() $testControlledVocab = Repo::controlledVocab()->build( ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - $assocId, - null + $assocId ); $controlledVocabEntryId1 = ControlledVocabEntry::create([ diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index 7075e295251..7e71fd19f8a 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -148,7 +148,7 @@ public function testValidateControlledVocabulary() { // Build a test vocabulary. (Assoc type and id are 0 to simulate a site-wide vocabulary). $vocab = Repo::controlledVocab()->build( - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 0, 0, null + ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, 0, 0 ); $controlledVocabEntry = ControlledVocabEntry::create([ diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index bb55f8ec812..3b760a8ccd7 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -37,8 +37,7 @@ public function testValidatorControlledVocab() $testControlledVocab = Repo::controlledVocab()->build( ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, Application::ASSOC_TYPE_CITATION, - $assocId, - null + $assocId ); $controlledVocabEntryId1 = ControlledVocabEntry::create([ From 0c9f0690da99e1f0175da6125925346925b8bda1 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 17 Dec 2024 19:22:03 +0600 Subject: [PATCH 27/30] pkp/pkp-lib#10292 fixing symbolics and settings name mixing confusion --- api/v1/vocabs/PKPVocabController.php | 8 ++++++-- classes/controlledVocab/ControlledVocab.php | 4 +--- .../controlledVocab/ControlledVocabEntry.php | 14 +++++++------- classes/core/PKPApplication.php | 2 ++ classes/core/traits/ModelWithSettings.php | 4 +++- classes/metadata/MetadataProperty.php | 2 +- classes/publication/DAO.php | 1 - classes/user/interest/Repository.php | 17 +++++++++-------- classes/user/interest/UserInterest.php | 2 -- 9 files changed, 29 insertions(+), 25 deletions(-) diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 27ffd343682..8a5cf38652f 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -25,6 +25,7 @@ use Illuminate\Support\Facades\Route; use PKP\controlledVocab\ControlledVocab; use PKP\controlledVocab\ControlledVocabEntry; +use PKP\controlledVocab\ControlledVocabEntryMatch; use PKP\core\PKPBaseController; use PKP\core\PKPRequest; use PKP\facades\Locale; @@ -118,11 +119,14 @@ public function getMany(Request $illuminateRequest): JsonResponse if (ControlledVocab::hasDefinedVocabSymbolic($vocab)) { $entries = ControlledVocabEntry::query() ->whereHas( - "controlledVocab", + 'controlledVocab', fn ($query) => $query->withSymbolics([$vocab])->withContextId($context->getId()) ) ->withLocales([$locale]) - ->when($term, fn ($query) => $query->withSetting($vocab, $term)) + ->when( + $term, + fn ($query) => $query->withSetting($vocab, $term, ControlledVocabEntryMatch::PARTIAL) + ) ->get(); } else { $entries = []; diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index ab2f4527c43..eb7d0f85732 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -158,10 +158,8 @@ public function scopeWithContextId(Builder $query, int $contextId): Builder * * @return array $controlledVocabEntryId => name */ - public function enumerate(?string $settingName = null): array + public function enumerate(string $settingName = 'name'): array { - $settingName ??= $this->symbolic; - return DB::table('controlled_vocab_entries AS e') ->leftJoin( 'controlled_vocab_entry_settings AS l', diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index 7ab3b9c4410..e8086a159b4 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -133,8 +133,8 @@ public function scopeWithLocales(Builder $query, array $locales): Builder { return $query->whereExists( fn ($query) => $query - ->select("{$this->primaryKey}") - ->from("{$this->getSettingsTable()}") + ->select($this->primaryKey) + ->from($this->getSettingsTable()) ->whereColumn( "{$this->getSettingsTable()}.{$this->primaryKey}", "{$this->getTable()}.{$this->primaryKey}" @@ -150,8 +150,8 @@ public function scopeWithSettings(Builder $query, string $settingName, array $se { return $query->whereExists( fn ($query) => $query - ->select("{$this->primaryKey}") - ->from("{$this->getSettingsTable()}") + ->select($this->primaryKey) + ->from($this->getSettingsTable()) ->whereColumn( "{$this->getSettingsTable()}.{$this->primaryKey}", "{$this->getTable()}.{$this->primaryKey}" @@ -175,13 +175,13 @@ public function scopeWithSetting( Builder $query, string $settingName, string $settingValue, - ControlledVocabEntryMatch $match = ControlledVocabEntryMatch::PARTIAL + ControlledVocabEntryMatch $match = ControlledVocabEntryMatch::EXACT ): Builder { return $query->whereExists( fn ($query) => $query - ->select("{$this->primaryKey}") - ->from("{$this->getSettingsTable()}") + ->select($this->primaryKey) + ->from($this->getSettingsTable()) ->whereColumn( "{$this->getSettingsTable()}.{$this->primaryKey}", "{$this->getTable()}.{$this->primaryKey}" diff --git a/classes/core/PKPApplication.php b/classes/core/PKPApplication.php index 2d8460cf04c..fc03f6038da 100644 --- a/classes/core/PKPApplication.php +++ b/classes/core/PKPApplication.php @@ -101,6 +101,7 @@ abstract class PKPApplication implements iPKPApplicationInfoProvider /** @deprecated 3.5 Use Application::SITE_CONTEXT_PATH, which had the value modified to "index" */ public const CONTEXT_ID_ALL = self::SITE_CONTEXT_ID_ALL; + public const ASSOC_TYPE_SITE = 0x0; public const ASSOC_TYPE_PRODUCTION_ASSIGNMENT = 0x0000202; public const ASSOC_TYPE_SUBMISSION_FILE = 0x0000203; public const ASSOC_TYPE_REVIEW_RESPONSE = 0x0000204; @@ -163,6 +164,7 @@ class_alias('\PKP\payment\QueuedPayment', '\QueuedPayment'); // QueuedPayment in 'ROUTE_COMPONENT', 'ROUTE_PAGE', 'ROUTE_API', 'CONTEXT_SITE', 'CONTEXT_ID_NONE', 'CONTEXT_ID_ALL', + 'ASSOC_TYPE_SITE', 'ASSOC_TYPE_PRODUCTION_ASSIGNMENT', 'ASSOC_TYPE_SUBMISSION_FILE', 'ASSOC_TYPE_REVIEW_RESPONSE', diff --git a/classes/core/traits/ModelWithSettings.php b/classes/core/traits/ModelWithSettings.php index cecf1f821cf..f59ab832a87 100644 --- a/classes/core/traits/ModelWithSettings.php +++ b/classes/core/traits/ModelWithSettings.php @@ -201,7 +201,9 @@ public function getAttribute($key): mixed return parent::getAttribute($key); } - return $this->isRelation($key) ? parent::getAttribute($key) : parent::getAttribute($this->getSnakeKey($key)); + return $this->isRelation($key) + ? parent::getAttribute($key) + : parent::getAttribute($this->getSnakeKey($key)); } /** diff --git a/classes/metadata/MetadataProperty.php b/classes/metadata/MetadataProperty.php index 9c18aaa091f..3ff3931fb8b 100644 --- a/classes/metadata/MetadataProperty.php +++ b/classes/metadata/MetadataProperty.php @@ -429,7 +429,7 @@ public function isValid($value, $locale = null) fn ($query) => $query->withSymbolics([$symbolic])->withAssoc($assocType, $assocId) ) ->withLocales([$locale]) - ->withSetting($symbolic, $value) + ->withSetting('name', $value) ->first(); if (!is_null($entry)) { diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 5706876e4eb..552a009df66 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -432,7 +432,6 @@ protected function saveControlledVocab(array $values, int $publicationId) protected function deleteControlledVocab(int $publicationId) { ControlledVocab::query() - ->withSymbolics(ControlledVocab::getDefinedVocabSymbolic()) ->withAssoc(Application::ASSOC_TYPE_PUBLICATION, $publicationId) ->delete(); } diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 80b6129e043..569bb3793f9 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -14,6 +14,7 @@ namespace PKP\user\interest; +use APP\core\Application; use APP\facades\Repo; use PKP\user\User; use PKP\user\interest\UserInterest; @@ -28,8 +29,8 @@ public function getAllInterests(?string $filter = null): array { $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, + Application::ASSOC_TYPE_SITE, + (int)Application::SITE_CONTEXT_ID, ); return ControlledVocabEntry::query() @@ -58,8 +59,8 @@ public function getInterestsForUser(User $user): array fn ($query) => $query ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID + Application::ASSOC_TYPE_SITE, + (int)Application::SITE_CONTEXT_ID, ) ) ->whereHas("userInterest", fn ($query) => $query->withUserId($user->getId())) @@ -89,8 +90,8 @@ public function setInterestsForUser(User $user, string|array|null $interests = n $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID, + Application::ASSOC_TYPE_SITE, + (int)Application::SITE_CONTEXT_ID, ); $currentInterests = ControlledVocabEntry::query() @@ -99,8 +100,8 @@ public function setInterestsForUser(User $user, string|array|null $interests = n fn ($query) => $query ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE, - UserInterest::CONTROLLED_VOCAB_INTEREST_ASSOC_ID + Application::ASSOC_TYPE_SITE, + (int)Application::SITE_CONTEXT_ID, ) ) ->withLocales(['']) diff --git a/classes/user/interest/UserInterest.php b/classes/user/interest/UserInterest.php index c6de5a76898..e3998715cb9 100644 --- a/classes/user/interest/UserInterest.php +++ b/classes/user/interest/UserInterest.php @@ -27,8 +27,6 @@ class UserInterest extends Model use HasCamelCasing; public const CONTROLLED_VOCAB_INTEREST = 'interest'; - public const CONTROLLED_VOCAB_INTEREST_ASSOC_TYPE = 0; - public const CONTROLLED_VOCAB_INTEREST_ASSOC_ID = 0; /** * @copydoc \Illuminate\Database\Eloquent\Model::$table From 8a8ad774c6246ccc890ed60e7c52da2401d69377 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Wed, 18 Dec 2024 18:03:17 +0600 Subject: [PATCH 28/30] pkp/pkp-lib#10292 fixed issue with user interest and multilingual data update through cast --- .../casts/MultilingualSettingAttribute.php | 2 +- classes/core/traits/EntityUpdate.php | 6 ----- classes/user/form/RolesForm.php | 7 +++++- classes/user/interest/Repository.php | 24 ++++++++++++++----- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/classes/core/casts/MultilingualSettingAttribute.php b/classes/core/casts/MultilingualSettingAttribute.php index 7e424ad1ebd..d91bfb1fd79 100644 --- a/classes/core/casts/MultilingualSettingAttribute.php +++ b/classes/core/casts/MultilingualSettingAttribute.php @@ -50,6 +50,6 @@ public function set(Model $model, string $key, mixed $value, array $attributes): return [$key => [Locale::getLocale() => $value]]; } - return [$key => array_filter($value)]; + return [$key => $value]; } } diff --git a/classes/core/traits/EntityUpdate.php b/classes/core/traits/EntityUpdate.php index 8102caf06a5..e5aa91a6142 100644 --- a/classes/core/traits/EntityUpdate.php +++ b/classes/core/traits/EntityUpdate.php @@ -76,12 +76,6 @@ public function updateSettings(array $props, int $modelId, $schema = null): void } if (!empty($propSchema->multilingual)) { - // first we will delete the settings entries for the locale keys which are not present in update query - DB::table($this->getSettingsTable()) - ->where($this->getPrimaryKeyName(), '=', $modelId) - ->where('setting_name', '=', $propName) - ->whereNotIn('locale', array_keys($props[$propName])) - ->delete(); foreach ($props[$propName] as $localeKey => $localeValue) { // Delete rows with a null value diff --git a/classes/user/form/RolesForm.php b/classes/user/form/RolesForm.php index e975491e209..6ab1cd28d84 100644 --- a/classes/user/form/RolesForm.php +++ b/classes/user/form/RolesForm.php @@ -17,6 +17,7 @@ namespace PKP\user\form; use APP\core\Application; +use APP\facades\Repo; use APP\template\TemplateManager; use PKP\user\User; use PKP\userGroup\UserGroup; @@ -42,7 +43,11 @@ public function fetch($request, $template = null, $display = false) { $templateMgr = TemplateManager::getManager($request); - $userGroupIds = UserGroup::getIdsByUserId($request->getUser()->getId()); + $userGroupIds = UserGroup::query() + ->withUserIds([$request->getUser()->getId()]) + ->get() + ->pluck('id') + ->toArray(); $templateMgr->assign('userGroupIds', $userGroupIds); diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 569bb3793f9..3307efe1988 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -44,7 +44,13 @@ public function getAllInterests(?string $filter = null): array ) ->get() ->sortBy(UserInterest::CONTROLLED_VOCAB_INTEREST) - ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST) + ->mapWithKeys( + fn(ControlledVocabEntry $controlledVocabEntry, int $id) => [ + $id => collect( + $controlledVocabEntry->{UserInterest::CONTROLLED_VOCAB_INTEREST} + )->first() + ] + ) ->toArray(); } @@ -65,7 +71,13 @@ public function getInterestsForUser(User $user): array ) ->whereHas("userInterest", fn ($query) => $query->withUserId($user->getId())) ->get() - ->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST, 'id') + ->mapWithKeys( + fn(ControlledVocabEntry $controlledVocabEntry, int $id) => [ + $id => collect( + $controlledVocabEntry->{UserInterest::CONTROLLED_VOCAB_INTEREST} + )->first() + ] + ) ->toArray(); } @@ -107,14 +119,14 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ->withLocales(['']) ->withSettings(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) ->get(); - - // Delete the existing interests association. + + // Delete user's existing interests association. UserInterest::query()->withUserId($user->getId())->delete(); - + $newInterestIds = collect( array_diff( $interests, - $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->toArray() + $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->flatten()->toArray() ) ) ->map(fn (string $interest): string => trim($interest)) From fb469a3002d460d2ef35e882058ddc0d06e13718 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Fri, 20 Dec 2024 14:50:50 +0600 Subject: [PATCH 29/30] pkp/pkp-lib#10292 controlled vocabs assoc id nullable --- classes/controlledVocab/ControlledVocab.php | 2 +- classes/controlledVocab/Repository.php | 6 +-- .../install/ControlledVocabMigration.php | 2 +- .../I10292_UpdateControlledVocabAssocId.php | 53 +++++++++++++++++++ classes/user/interest/Repository.php | 8 +-- 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabAssocId.php diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index eb7d0f85732..6af1ea1e8e7 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -123,7 +123,7 @@ public function scopeWithSymbolics(Builder $query, array $symbolics): Builder /** * Scope a query to only include vocabs with a specific assoc type and assoc ID. */ - public function scopeWithAssoc(Builder $query, int $assocType, int $assocId): Builder + public function scopeWithAssoc(Builder $query, int $assocType, ?int $assocId): Builder { return $query->where('assoc_type', $assocType)->where('assoc_id', $assocId); } diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index fa1a7d3db0e..5ebdd2c4095 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -26,7 +26,7 @@ class Repository public function build( string $symbolic, int $assocType, - int $assocId + ?int $assocId ): ControlledVocab { return ControlledVocab::query() @@ -45,7 +45,7 @@ public function build( public function getBySymbolic( string $symbolic, int $assocType, - int $assocId, + ?int $assocId, ?array $locales = [] ): array { @@ -74,7 +74,7 @@ public function insertBySymbolic( string $symbolic, array $vocabs, int $assocType, - int $assocId, + ?int $assocId, bool $deleteFirst = true, ): void { diff --git a/classes/migration/install/ControlledVocabMigration.php b/classes/migration/install/ControlledVocabMigration.php index 431184bf15a..29e4e2f04db 100644 --- a/classes/migration/install/ControlledVocabMigration.php +++ b/classes/migration/install/ControlledVocabMigration.php @@ -30,7 +30,7 @@ public function up(): void $table->bigInteger('controlled_vocab_id')->autoIncrement(); $table->string('symbolic', 64); $table->bigInteger('assoc_type')->default(0); - $table->bigInteger('assoc_id')->default(0); + $table->bigInteger('assoc_id')->nullable(); $table->unique(['symbolic', 'assoc_type', 'assoc_id'], 'controlled_vocab_symbolic'); }); diff --git a/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabAssocId.php b/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabAssocId.php new file mode 100644 index 00000000000..8871db22f9d --- /dev/null +++ b/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabAssocId.php @@ -0,0 +1,53 @@ +bigInteger('assoc_id')->nullable(true)->change(); + }); + + DB::table('controlled_vocabs') + ->where('symbolic', UserInterest::CONTROLLED_VOCAB_INTEREST) + ->update(['assoc_id' => Application::SITE_CONTEXT_ID]); + } + + /** + * Reverse the migration. + */ + public function down(): void + { + DB::table('controlled_vocabs') + ->where('symbolic', UserInterest::CONTROLLED_VOCAB_INTEREST) + ->update(['assoc_id' => 0]); + + Schema::table('controlled_vocabs', function (Blueprint $table) { + $table->bigInteger('assoc_id')->nullable(false)->change(); + }); + } +} diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 3307efe1988..3923391efc3 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -30,7 +30,7 @@ public function getAllInterests(?string $filter = null): array $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST, Application::ASSOC_TYPE_SITE, - (int)Application::SITE_CONTEXT_ID, + Application::SITE_CONTEXT_ID, ); return ControlledVocabEntry::query() @@ -66,7 +66,7 @@ public function getInterestsForUser(User $user): array ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( Application::ASSOC_TYPE_SITE, - (int)Application::SITE_CONTEXT_ID, + Application::SITE_CONTEXT_ID, ) ) ->whereHas("userInterest", fn ($query) => $query->withUserId($user->getId())) @@ -103,7 +103,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n $controlledVocab = Repo::controlledVocab()->build( UserInterest::CONTROLLED_VOCAB_INTEREST, Application::ASSOC_TYPE_SITE, - (int)Application::SITE_CONTEXT_ID, + Application::SITE_CONTEXT_ID, ); $currentInterests = ControlledVocabEntry::query() @@ -113,7 +113,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST]) ->withAssoc( Application::ASSOC_TYPE_SITE, - (int)Application::SITE_CONTEXT_ID, + Application::SITE_CONTEXT_ID, ) ) ->withLocales(['']) From 94148594b3e7f830780d482eb1f4f39e910118a8 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Fri, 20 Dec 2024 18:58:53 +0600 Subject: [PATCH 30/30] pkp/pkp-lib#10292 migrated controlled vocabs setting name from symbolics to name --- api/v1/vocabs/PKPVocabController.php | 4 +- classes/controlledVocab/ControlledVocab.php | 8 ---- .../controlledVocab/ControlledVocabEntry.php | 12 +----- classes/controlledVocab/Repository.php | 8 ++-- ..._UpdateControlledVocabEntrySettingName.php | 39 +++++++++++++++++++ classes/user/interest/Repository.php | 19 +++------ .../FormValidatorControlledVocabTest.php | 4 +- .../classes/metadata/MetadataPropertyTest.php | 2 +- .../ValidatorControlledVocabTest.php | 4 +- 9 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabEntrySettingName.php diff --git a/api/v1/vocabs/PKPVocabController.php b/api/v1/vocabs/PKPVocabController.php index 8a5cf38652f..465a5087282 100644 --- a/api/v1/vocabs/PKPVocabController.php +++ b/api/v1/vocabs/PKPVocabController.php @@ -116,7 +116,7 @@ public function getMany(Request $illuminateRequest): JsonResponse ], Response::HTTP_BAD_REQUEST); } - if (ControlledVocab::hasDefinedVocabSymbolic($vocab)) { + if (in_array($vocab, ControlledVocab::getDefinedVocabSymbolic())) { $entries = ControlledVocabEntry::query() ->whereHas( 'controlledVocab', @@ -135,7 +135,7 @@ public function getMany(Request $illuminateRequest): JsonResponse $data = []; foreach ($entries as $entry) { - $data[] = $entry->getLocalizedData($vocab, $locale); + $data[] = $entry->getLocalizedData('name', $locale); } $data = array_values(array_unique($data)); diff --git a/classes/controlledVocab/ControlledVocab.php b/classes/controlledVocab/ControlledVocab.php index 6af1ea1e8e7..380e92705a1 100644 --- a/classes/controlledVocab/ControlledVocab.php +++ b/classes/controlledVocab/ControlledVocab.php @@ -96,14 +96,6 @@ public static function getDefinedVocabSymbolic(): array ]; } - /** - * Check if a provided vocab symbolic defined in pre defined symbolic list - */ - public static function hasDefinedVocabSymbolic(string $vocab): bool - { - return in_array($vocab, static::getDefinedVocabSymbolic()); - } - /** * Get all controlled vocab entries for this controlled vocab */ diff --git a/classes/controlledVocab/ControlledVocabEntry.php b/classes/controlledVocab/ControlledVocabEntry.php index e8086a159b4..052f98d8e91 100644 --- a/classes/controlledVocab/ControlledVocabEntry.php +++ b/classes/controlledVocab/ControlledVocabEntry.php @@ -83,11 +83,7 @@ public static function getSchemaName(): ?string */ public function getMultilingualProps(): array { - return array_merge( - $this->multilingualProps, - ControlledVocab::getDefinedVocabSymbolic(), - Arr::wrap(UserInterest::CONTROLLED_VOCAB_INTEREST) - ); + return array_merge($this->multilingualProps, ['name']); } /** @@ -95,11 +91,7 @@ public function getMultilingualProps(): array */ public function getSettings(): array { - return array_merge( - $this->settings, - ControlledVocab::getDefinedVocabSymbolic(), - Arr::wrap(UserInterest::CONTROLLED_VOCAB_INTEREST) - ); + return array_merge($this->settings, ['name']); } /** diff --git a/classes/controlledVocab/Repository.php b/classes/controlledVocab/Repository.php index 5ebdd2c4095..04ae0758b5c 100644 --- a/classes/controlledVocab/Repository.php +++ b/classes/controlledVocab/Repository.php @@ -53,13 +53,13 @@ public function getBySymbolic( ControlledVocabEntry::query() ->whereHas( - "controlledVocab", + 'controlledVocab', fn ($query) => $query->withSymbolics([$symbolic])->withAssoc($assocType, $assocId) ) ->when(!empty($locales), fn ($query) => $query->withLocales($locales)) ->get() - ->each(function ($entry) use (&$result, $symbolic) { - foreach ($entry->{$symbolic} as $locale => $value) { + ->each(function ($entry) use (&$result) { + foreach ($entry->name as $locale => $value) { $result[$locale][] = $value; } }); @@ -98,7 +98,7 @@ public function insertBySymbolic( ControlledVocabEntry::create([ 'controlledVocabId' => $controlledVocab->id, 'seq' => $index + 1, - "{$symbolic}" => [ + 'name' => [ $locale => $vocab ], ]) diff --git a/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabEntrySettingName.php b/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabEntrySettingName.php new file mode 100644 index 00000000000..636275f3a06 --- /dev/null +++ b/classes/migration/upgrade/v3_5_0/I10292_UpdateControlledVocabEntrySettingName.php @@ -0,0 +1,39 @@ +update(['setting_name' => 'name']); + } + + /** + * Reverse the migration. + */ + public function down(): void + { + throw new DowngradeNotSupportedException(); + } +} diff --git a/classes/user/interest/Repository.php b/classes/user/interest/Repository.php index 3923391efc3..c4b917d720a 100644 --- a/classes/user/interest/Repository.php +++ b/classes/user/interest/Repository.php @@ -37,18 +37,13 @@ public function getAllInterests(?string $filter = null): array ->withControlledVocabId($controlledVocab->id) ->when( $filter, - fn ($query) => $query->withSetting( - UserInterest::CONTROLLED_VOCAB_INTEREST, - $filter - ) + fn ($query) => $query->withSetting('name', $filter) ) ->get() ->sortBy(UserInterest::CONTROLLED_VOCAB_INTEREST) ->mapWithKeys( fn(ControlledVocabEntry $controlledVocabEntry, int $id) => [ - $id => collect( - $controlledVocabEntry->{UserInterest::CONTROLLED_VOCAB_INTEREST} - )->first() + $id => collect($controlledVocabEntry->name)->first() ] ) ->toArray(); @@ -73,9 +68,7 @@ public function getInterestsForUser(User $user): array ->get() ->mapWithKeys( fn(ControlledVocabEntry $controlledVocabEntry, int $id) => [ - $id => collect( - $controlledVocabEntry->{UserInterest::CONTROLLED_VOCAB_INTEREST} - )->first() + $id => collect($controlledVocabEntry->name)->first() ] ) ->toArray(); @@ -117,7 +110,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ) ) ->withLocales(['']) - ->withSettings(UserInterest::CONTROLLED_VOCAB_INTEREST, $interests) + ->withSettings('name', $interests) ->get(); // Delete user's existing interests association. @@ -126,7 +119,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n $newInterestIds = collect( array_diff( $interests, - $currentInterests->pluck(UserInterest::CONTROLLED_VOCAB_INTEREST)->flatten()->toArray() + $currentInterests->pluck('name')->flatten()->toArray() ) ) ->map(fn (string $interest): string => trim($interest)) @@ -134,7 +127,7 @@ public function setInterestsForUser(User $user, string|array|null $interests = n ->map( fn (string $interest) => ControlledVocabEntry::create([ 'controlledVocabId' => $controlledVocab->id, - UserInterest::CONTROLLED_VOCAB_INTEREST => [ + 'name' => [ '' => $interest ], ])->id diff --git a/tests/classes/form/validation/FormValidatorControlledVocabTest.php b/tests/classes/form/validation/FormValidatorControlledVocabTest.php index c837efe9bb9..dcb60b01d99 100644 --- a/tests/classes/form/validation/FormValidatorControlledVocabTest.php +++ b/tests/classes/form/validation/FormValidatorControlledVocabTest.php @@ -47,14 +47,14 @@ public function testIsValid() $controlledVocabEntryId1 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'name' => [ 'en' => 'testEntry', ], ])->id; $controlledVocabEntryId2 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'name' => [ 'en' => 'testEntry', ], ])->id; diff --git a/tests/classes/metadata/MetadataPropertyTest.php b/tests/classes/metadata/MetadataPropertyTest.php index 7e71fd19f8a..4af2ee540d6 100644 --- a/tests/classes/metadata/MetadataPropertyTest.php +++ b/tests/classes/metadata/MetadataPropertyTest.php @@ -153,7 +153,7 @@ public function testValidateControlledVocabulary() $controlledVocabEntry = ControlledVocabEntry::create([ 'controlledVocabId' => $vocab->id, - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'name' => [ 'en' => 'testEntry', ], ]); diff --git a/tests/classes/validation/ValidatorControlledVocabTest.php b/tests/classes/validation/ValidatorControlledVocabTest.php index 3b760a8ccd7..b1d3afd05d8 100644 --- a/tests/classes/validation/ValidatorControlledVocabTest.php +++ b/tests/classes/validation/ValidatorControlledVocabTest.php @@ -42,14 +42,14 @@ public function testValidatorControlledVocab() $controlledVocabEntryId1 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'name' => [ 'en' => 'testEntry', ], ])->id; $controlledVocabEntryId2 = ControlledVocabEntry::create([ 'controlledVocabId' => $testControlledVocab->id, - ControlledVocab::CONTROLLED_VOCAB_SUBMISSION_KEYWORD => [ + 'name' => [ 'en' => 'testEntry', ], ])->id;